Unable to export commonjs module that is a generic class with static members that are generic classes

Topics: Language Specification
Jan 9, 2014 at 10:16 PM
Edited Jan 9, 2014 at 11:22 PM
I'm trying to write a CommonJS declaration file for Bluebird, a promise library that directly exports a generic Promise class. However, the library also exports several other generic classes as static members (PromiseInspection), and it seems like its impossible to model this with typescript.

Edit: Usage example, to illustrate how the module's exported class works:
import Promise = require('bluebird');
var promise:Promise<number> = Promise.cast(5);
var x:Promise.PromiseInspection<number> = promise.inspect();
I tried several strategies - simplified examples follow:

1. The obvious way

declare module "bluebird" {
    class PromiseInspection<T> {
        // ...
    }
    class Promise<T> {
        PromiseInspection: typeof PromiseInspection; // error
        constructor<T>();
        inspect():PromiseInspection<T>; // error
        static cast<U>(value:U):Promise<U>; 
        // ...
    }
    export = Promise;
}
Fails with the error unable to use private type PromiseInspection as a public property

2. Using a static interface

declare module "bluebird2" {
    interface PromiseInspection<T> {
        // ...  
    }
    interface Promise<T> {
        constructor<T>();
        inspect():PromiseInspection<T>;
    }
    interface PromiseStatic {
        new<T>();
        PromiseInspection:typeof PromiseInspection;
        cast<U>(value:U):Promise<U>; // error
    }
    export = PromiseStatic;
}
Also fails similarly, but this time the private type is Promise<T>

3. Trying to directly export a constructor function from the module

declare module "bluebird3" {
    export interface PromiseInspection<T> {
        // ...
    }
    export interface Promise<T> {
        constructor<T>();
        inspect():PromiseInspection<T>;
    }

    export new<T>(); // syntax error
    export function cast<U>(value:U):Promise<U>; 
}
This almost works, except of course its impossible to a constructor function that way.

4. The namespace polluting way (Works, with downsides)

interface PromiseInspection<T> {
    // ...
}
interface Promise<T> {
    constructor<T>();
    inspect():PromiseInspection<T>;
}

declare module "bluebird4" {    
    interface PromiseStatic {
        new<T>():Promise<T>;
        PromiseInspection: typeof PromiseInspection;
        cast<U>(value:U):Promise<U>;
    }
    export = PromiseStatic;
}
Works, but it pollutes the global namespace with both Promise and PromiseInspection. This might be okay but I'd rather avoid it as in CommonJS its usually considered unacceptable.

5. With declaration merging (gets me 90% of the way...)

declare module "bluebird5" {
    module Promise {
        export interface PromiseInspection<T> {
            value(): T;
            // ...
        }
        export
        function cast<U>(value: U): Promise<U> ;
    }

    class Promise<T> {
        new <T> (): Promise <T> ;
        inspect(): Promise.PromiseInspection <T> ;
    }

    export = Promise;
}
Almost there - except that now I'm not allowed to replace class Promise<T> with interface Promise<T>, making Promise<T> unextendable. If I try to do it, the following code:
import Promise = require('bluebird');
var x = new Promise<number>();
x.inspect().value().toExponential();
fails with the error "Invalid 'new' expression".

Another note: it was completely unintuitive to me that I had to use the Promise. prefix from inside the promise class declaration. I was expecting that the compiler will recognize that the declarations gets merged and allow me to simply use PromiseInspection

Link to the actual, work-in-progress bluebird.d.ts - this one currently pollutes the global namespace (uses solution 4)

Is there a better way to do this, or did I hit a language limitation?
Jan 9, 2014 at 10:40 PM
Edited Jan 9, 2014 at 10:48 PM
  • merged into question -
Developer
Jan 10, 2014 at 4:32 AM
Writing your 'bluebird.d.ts' like this should do it:
declare module Promise {
    export interface PromiseInspection<T> {
        value(): T;
        // ...
    }
}

interface Promise<T> {
    inspect(): Promise.PromiseInspection<T>;
}

declare var Promise: {
    new <T>(): Promise<T>;
    cast<U>(value: U): Promise<U>;
}

export = Promise;
Writing it this way you have a separate declaration for each of the three meanings of the identifier Promise: As a namespace (a module containing only types), as a type (that happens to be generic), and as a value. There is no need to have a 'declare module "bluebird" { }' around the whole thing. The fact that you have an export assignment automatically causes the compiler to treat the file as an external module.
Jan 10, 2014 at 12:19 PM
Edited Jan 10, 2014 at 12:35 PM
Thanks, the compiler accepted that! Still, isn't it reasonable to expect that PromiseInspection wont need a namespace prefix if the declarations get merged? Prefixing everything inside interface Promise<T> { ... } is going to get very tedious, very fast!

Also as far as I can see, this means that I will have to export all class constructors from the var declaration, i.e.
RejectionError: new() => Promise.RejectionError;
Wouldn't everything be much simpler if the language allowed exporting a constructor function from the module? i.e.
declare module Promise {
    export new<T>():Promise<T>
}
Now about the wrapper - I added declare module "bluebird" to be able to use the module in node.js by simply typing import Promise = require('bluebird').

Then I'd pass _references.d.ts to the compiler containing

///<reference path="typedefs/bluebird/bluebird.d.ts">

The compiler will now happily translate the import statement to var Promise = require('bluebird'), and node.js will happily use that to load the correct js file using its module lookup algorithm (will look up the main field in node_modules/bluebird/package.json, then load that file).

This is the only way I know of to make both happy, since tsc and node don't share the same lookup algorithm for module names that aren't prefixed with a dot. Or is there a better way?