Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TypeScript definitions #86

Merged
merged 16 commits into from
Sep 2, 2022
81 changes: 81 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true
}
}