node.jstypescriptdefinitelytypednode-promisify

Type functions that can be promsified


I'm working on adding separate types to node-vagrant NPM library to use in Typescript, to be contributed under DefinitelyTyped. However, one of the methods of the library is promisify which makes all other functions in the library then return a promise instead of a callback.

Given that I only have control over adding the typing (.d.ts) file, is there a way to indicate to tsc that the result of calling that custom promisify function is on the functions, or some other mechanism of dynamism? Or is it just that I provide typings for both usages of the function, and the user has to just make sure they choose appropriately?

A minimal example would be for the JS file to be:

module.exports.foo = function (cb) {
    cb(null, 'foo');
};

module.exports.promisify = function () {
  module.exports.foo = util.promisify(module.exports.foo);
}

and the typing (in a .d.ts file) I've got is:

export function foo(cb: (err: null | string, out: string) => void): void;
export function promisify(): void;

Now, when I use the typings:

import foo = require('foo');

foo.foo((err, out) => { console.log(err, out); });
foo.promisify();
foo.foo().then(out => console.log(out)).catch(err => console.log(err));

where the last line throws an error with the TSC. Is the solution to just declare both callback and the promise on the function signature and have the end user appropriately decide on which one to use or is there some mechanism in TypeScript to dynamically toggle the return info on a function?

From the above, is the final verdict just doing:

export function foo(): Promise<string>;
export function foo(cb: (err: null | string, out: string) => void): void;

and and as mentioned above, just letting the end-user figure out if they want the callback or the promise?


Solution

  • Given that I only have control over adding the typing (.d.ts) file, is there a way to indicate to tsc that the result of calling that custom promisify function is on the functions, or some other mechanism of dynamism?

    As far as I'm aware this is not possible with TypeScript. The type of foo is static and while TypeScript does some control flow based type analysis it can't switch the type of foo using a function call.


    It would be a lot easier to create accurate type definitions for this if promisify() returned a new object holding the promisified functions instead of switching them out in-place. But since you don't have control over the source the only two options I see are:

    Overloads

    This is the solution you already mention in your question. By declaring two signatures for each method all usages will be valid but the user has the responsibility to make sure they call promisify if they want to use promises.

    export function foo(): Promise<string>;
    export function foo(cb: (err: null | string, out: string) => void): void;
    

    Union

    Alternatively the whole module could export a union type, requiring the consumer to cast it before using the promisified API. It could look something like this:

    export interface CallbackFoo {
        foo(cb: (err: null | string, out: string) => void): void;
        promisify(): void;
    }
    
    export interface PromiseFoo {
        foo(): Promise<string>;
        promisify(): void;
    }
    
    declare const _: CallbackFoo | PromiseFoo;
    export default _;
    

    And the usage:

    import foo, { PromiseFoo } from 'foo';
    
    foo.foo((err, out) => { console.log(err, out); });
    foo.promisify();
    (foo as PromiseFoo).foo().then(out => console.log(out)).catch(err => console.log(err));
    

    Now you probably wouldn't want to cast it all the time when using it. A cleaner solution could be to create a separate file which imports foo, calls promisify and exports it:

    import foo, { PromiseFoo } from './foo';
    
    foo.promisify();
    
    export default foo as PromiseFoo;
    

    However this would of course have to be on the end-user's side.