Skip to content

Commit

Permalink
Add TypeScript definitions (#86)
Browse files Browse the repository at this point in the history
  • Loading branch information
tom-sherman committed Sep 2, 2022
1 parent 513b0d5 commit e13efc7
Show file tree
Hide file tree
Showing 5 changed files with 279 additions and 2 deletions.
81 changes: 81 additions & 0 deletions index.d.ts
@@ -0,0 +1,81 @@
/* eslint-disable @typescript-eslint/ban-types */

type Last<T extends readonly unknown[]> = T extends [...any, infer L]
? L
: never;
type DropLast<T extends readonly unknown[]> = T extends [...(infer U), any]
? U
: [];

type StringEndsWith<S, X extends string> = S extends `${infer _}${X}` ? true : false;

interface Options<Includes extends readonly unknown[], Excludes extends readonly unknown[], MultiArgs extends boolean = false, ErrorFirst extends boolean = true, ExcludeMain extends boolean = false> {
multiArgs?: MultiArgs;
include?: Includes;
exclude?: Excludes;
errorFirst?: ErrorFirst;
promiseModule?: PromiseConstructor;
excludeMain?: ExcludeMain;
}

interface InternalOptions<Includes extends readonly unknown[], Excludes extends readonly unknown[], MultiArgs extends boolean = false, ErrorFirst extends boolean = true> {
multiArgs: MultiArgs;
include: Includes;
exclude: Excludes;
errorFirst: ErrorFirst;
}

type Promisify<Args extends readonly unknown[], GenericOptions extends InternalOptions<readonly unknown[], readonly unknown[], boolean, boolean>> = (
...args: DropLast<Args>
) =>
Last<Args> extends (...args: any) => any
// For single-argument functions when errorFirst: true we just return Promise<unknown> as it will always reject.
? Parameters<Last<Args>> extends [infer SingleCallbackArg] ? GenericOptions extends {errorFirst: true} ? Promise<unknown> : Promise<SingleCallbackArg>
: Promise<
GenericOptions extends {multiArgs: false}
? Last<Parameters<Last<Args>>>
: Parameters<Last<Args>>
>
// Functions without a callback will return a promise that never settles. We model this as Promise<unknown>
: Promise<unknown>;

type PromisifyModule<
Module extends Record<string, any>,
MultiArgs extends boolean,
ErrorFirst extends boolean,
Includes extends ReadonlyArray<keyof Module>,
Excludes extends ReadonlyArray<keyof Module>,
> = {
[K in keyof Module]: Module[K] extends (...args: infer Args) => any
? K extends Includes[number]
? Promisify<Args, InternalOptions<Includes, Excludes, MultiArgs>>
: K extends Excludes[number]
? Module[K]
: StringEndsWith<K, 'Sync' | 'Stream'> extends true
? Module[K]
: Promisify<Args, InternalOptions<Includes, Excludes, MultiArgs, ErrorFirst>>
: Module[K];
};

declare function pify<
FirstArg,
Args extends readonly unknown[],
MultiArgs extends boolean = false,
ErrorFirst extends boolean = true,
>(
input: (arg: FirstArg, ...args: Args) => any,
options?: Options<[], [], MultiArgs, ErrorFirst>
): Promisify<[FirstArg, ...Args], InternalOptions<[], [], MultiArgs, ErrorFirst>>;
declare function pify<
Module extends Record<string, any>,
Includes extends ReadonlyArray<keyof Module> = [],
Excludes extends ReadonlyArray<keyof Module> = [],
MultiArgs extends boolean = false,
ErrorFirst extends boolean = true,
>(
// eslint-disable-next-line unicorn/prefer-module
module: Module,
options?: Options<Includes, Excludes, MultiArgs, ErrorFirst, true>
): PromisifyModule<Module, MultiArgs, ErrorFirst, Includes, Excludes>;

export = pify;
171 changes: 171 additions & 0 deletions index.test-d.ts
@@ -0,0 +1,171 @@
import {expectError, expectType, printType} from 'tsd';
import pify from '.';

expectError(pify());
expectError(pify(null));
expectError(pify(undefined));
expectError(pify(123));
expectError(pify('abc'));
expectError(pify(null, {}));
expectError(pify(undefined, {}));
expectError(pify(123, {}));
expectError(pify('abc', {}));

// eslint-disable-next-line @typescript-eslint/no-empty-function
expectType<Promise<unknown>>(pify((v: number) => {})());
expectType<Promise<unknown>>(pify(() => 'hello')());

// Callback with 1 additional params
declare function fn1(x: number, fn: (error: Error, value: number) => void): void;
expectType<Promise<number>>(pify(fn1)(1));

// Callback with 2 additional params
declare function fn2(x: number, y: number, fn: (error: Error, value: number) => void): void;
expectType<Promise<number>>(pify(fn2)(1, 2));

// Generics

declare function generic<T>(value: T, fn: (error: Error, value: T) => void): void;
declare const genericValue: 'hello' | 'goodbye';
expectType<Promise<typeof genericValue>>(pify(generic)(genericValue));

declare function generic10<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(
value1: T1,
value2: T2,
value3: T3,
value4: T4,
value5: T5,
value6: T6,
value7: T7,
value8: T8,
value9: T9,
value10: T10,
cb: (error: Error, value: {
val1: T1;
val2: T2;
val3: T3;
val4: T4;
val5: T5;
val6: T6;
val7: T7;
val8: T8;
val9: T9;
val10: T10;
}) => void
): void;
expectType<
Promise<{
val1: 1;
val2: 2;
val3: 3;
val4: 4;
val5: 5;
val6: 6;
val7: 7;
val8: '8';
val9: 9;
val10: 10;
}>
>(pify(generic10)(1, 2, 3, 4, 5, 6, 7, '8', 9, 10));

// MultiArgs
declare function callback02(cb: (x: number, y: string) => void): void;
declare function callback12(value: 'a', cb: (x: number, y: string) => void): void;
declare function callback22(
value1: 'a',
value2: 'b',
cb: (x: number, y: string) => void
): void;

expectType<Promise<[number, string]>>(pify(callback02, {multiArgs: true})());
expectType<Promise<[number, string]>>(
pify(callback12, {multiArgs: true})('a'),
);
expectType<Promise<[number, string]>>(
pify(callback22, {multiArgs: true})('a', 'b'),
);

// Overloads
declare function overloaded(value: number, cb: (error: Error, value: number) => void): void;
declare function overloaded(value: string, cb: (error: Error, value: string) => void): void;

// Chooses last overload
// See https://github.com/microsoft/TypeScript/issues/32164
expectType<Promise<string>>(pify(overloaded)(''));

declare const fixtureModule: {
method1: (arg: string, cb: (error: Error, value: string) => void) => void;
method2: (arg: number, cb: (error: Error, value: number) => void) => void;
method3: (arg: string) => string;
methodSync: (arg: 'sync') => 'sync';
methodStream: (arg: 'stream') => 'stream';
callbackEndingInSync: (arg: 'sync', cb: (error: Error, value: 'sync') => void) => void;
prop: number;
};

// Module support
expectType<number>(pify(fixtureModule).prop);
expectType<Promise<string>>(pify(fixtureModule).method1(''));
expectType<Promise<number>>(pify(fixtureModule).method2(0));
// Same semantics as pify(fn)
expectType<Promise<unknown>>(pify(fixtureModule).method3());

// Excludes
expectType<
(arg: string, cb: (error: Error, value: string) => void) => void
>(pify(fixtureModule, {exclude: ['method1']}).method1);

// Includes
expectType<Promise<string>>(pify(fixtureModule, {include: ['method1']}).method1(''));
expectType<Promise<number>>(pify(fixtureModule, {include: ['method2']}).method2(0));

// Excludes sync and stream method by default
expectType<
(arg: 'sync') => 'sync'
>(pify(fixtureModule, {exclude: ['method1']}).methodSync);
expectType<
(arg: 'stream') => 'stream'
>(pify(fixtureModule, {exclude: ['method1']}).methodStream);

// Include sync method
expectType<
(arg: 'sync') => Promise<'sync'>
>(pify(fixtureModule, {include: ['callbackEndingInSync']}).callbackEndingInSync);

// Option errorFirst:

declare function fn0(fn: (value: number) => void): void;

// Unknown as it returns a promise that always rejects because errorFirst = true
expectType<Promise<unknown>>(pify(fn0)());
expectType<Promise<unknown>>(pify(fn0, {errorFirst: true})());

expectType<Promise<number>>(pify(fn0, {errorFirst: false})());
expectType<Promise<[number, string]>>(pify(callback02, {multiArgs: true, errorFirst: true})());
expectType<Promise<[number, string]>>(
pify(callback12, {multiArgs: true, errorFirst: false})('a'),
);
expectType<Promise<[number, string]>>(
pify(callback22, {multiArgs: true, errorFirst: false})('a', 'b'),
);

// Module function

// eslint-disable-next-line @typescript-eslint/no-empty-function
function moduleFunction(_cb: (error: Error, value: number) => void): void {}
// eslint-disable-next-line @typescript-eslint/no-empty-function
moduleFunction.method = function (_cb: (error: Error, value: string) => void): void {};

expectType<Promise<number>>(pify(moduleFunction)());

expectType<Promise<string>>(pify(moduleFunction, {excludeMain: true}).method());

// Classes

declare class MyClass {
method1(cb: (error: Error, value: string) => void): void;
method2(arg: number, cb: (error: Error, value: number) => void): void;
}

expectType<Promise<string>>(pify(new MyClass()).method1());
expectType<Promise<number>>(pify(new MyClass()).method2(4));
7 changes: 5 additions & 2 deletions package.json
Expand Up @@ -16,11 +16,12 @@
"node": ">=14.16"
},
"scripts": {
"test": "xo && ava",
"test": "xo && ava && tsd",
"optimization-test": "node --allow-natives-syntax optimization-test.js"
},
"files": [
"index.js"
"index.js",
"index.d.ts"
],
"keywords": [
"promisify",
Expand All @@ -45,6 +46,8 @@
"devDependencies": {
"ava": "^4.3.0",
"pinkie-promise": "^2.0.1",
"tsd": "^0.23.0",
"typescript": "^4.8.2",
"v8-natives": "^1.2.5",
"xo": "^0.49.0"
}
Expand Down
16 changes: 16 additions & 0 deletions readme.md
Expand Up @@ -142,6 +142,22 @@ someClassPromisified.someFunction();
const someFunction = pify(someClass.someFunction.bind(someClass));
```

#### With TypeScript why is `pify` choosing the last function overload?

If you're using TypeScript and your input has [function overloads](https://www.typescriptlang.org/docs/handbook/2/functions.html#function-overloads) then only the last overload will be chosen and promisified.

If you need to choose a different overload consider using a type assertion eg.

```ts
function overloadedFunction(input: number, cb: (error: unknown, data: number => void): void
function overloadedFunction(input: string, cb: (error: unknown, data: string) => void): void
/* ... */
}

const fn = pify(overloadedFunction as (input: number, cb: (error: unknown, data: number) => void) => void)
// ^ ? (input: number) => Promise<number>
```
## Related
- [p-event](https://github.com/sindresorhus/p-event) - Promisify an event by waiting for it to be emitted
Expand Down
6 changes: 6 additions & 0 deletions tsconfig.json
@@ -0,0 +1,6 @@
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true
}
}

0 comments on commit e13efc7

Please sign in to comment.