Alec Mev
Alec Mev

Reputation: 4813

Typing a CommonJS module that exports a function constructor with static methods

I'm trying to create a declaration file for minimal-bit-array. Here's its essence:

function A() { this.data = 'foo'; }

A.prototype.serialize = function() { /* ... */ };

module.exports = A;
module.exports.deserialize = function(x) { /* ... */ };

How do I type this?

Upvotes: 2

Views: 407

Answers (1)

Alec Mev
Alec Mev

Reputation: 4813

This is how:

declare module "a" {
  namespace A {
    export interface X { /* ... */ }
  }

  interface A {
    data: string;
    serialize(): A.X;
  }

  const A: {
    new (): A;
    deserialize(obj: A.X): A;
  };

  export = A;
}

This counter-intuitive namespace + interface + const sandwich gets successfully merged into an unambiguous something that TypeScript can understand. Consumption example:

import * as A from "a";

const a: A         = new A();
const d: A['data'] = a.data;
const x: A.X       = a.serialize();
const amazing      = A.deserialize(x);

Why the sandwich?

Theoretically, namespace A and const A are doing the same thing: describing module.exports of a. But the constructor - new (): A - can be declared only in an interface, while the TypeScript-only stuff - interface X - can be defined only in a namespace. That's why both are needed at the same time. There's no way to export interface X otherwise. At least as far as I know. Thankfully, TypeScript knows how to merge them.

In the meanwhile, interface A is just a convenience. When referencing A somewhere in the code in a context where a type would be expected, this is the interface that you get. The other two - namespace and const - aren't types, so there's no ambiguity. Could keep it in namespace A, however, if this is too much magic, and reference it as A.A.

Tested on 2.8.

Update

A slightly cleaner approach:

declare module "a" {
  namespace A {
    export interface X { /* ... */ }
  }

  class A {
    static deserialize(obj: A.X): A;
    data: string;
    constructor();
    serialize(): A.X;
  }

  export = A;
}

Classes happen to fit this scenario nicely. This is semantically equivalent to the original approach too, since classes are essentially syntactic sugar on top of functions. Check this playground to see what an implementation of the class above would get transpiled into.

Tested on 2.8.

Upvotes: 4

Related Questions