Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript: Cannot export a module that is a generic interface and contains other generic interfaces

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

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"

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?

like image 362
Gjorgi Kjosev Avatar asked Jan 09 '14 13:01

Gjorgi Kjosev


1 Answers

Anders Hejlsberg posted an answer on CodePlex, so I'm going to add it here. The declaration merging solution was close - but I also needed a "var" declaration to declare the static interface as it is the only one that can accept a constructor function.

declare module "bluebird" {
    module Promise {
        export interface PromiseInspection<T> {
            value(): T;
        }
    }
    interface Promise<T> {
        inspect(): Promise.PromiseInspection <T> ;
    }

    var Promise: {
        new<U>(): Promise<U>;
        cast<U>(value: U): Promise<U> ;
    }
    export = Promise;
}

So basically:

  • interface members in the module declaration (as long as they declare just types i.e. non-physical)
  • instance members in the main interface
  • static function members, the constructor and other "physical" members in the var declaration.

Also, his comment:

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.

like image 197
Gjorgi Kjosev Avatar answered Nov 16 '22 00:11

Gjorgi Kjosev