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
78 changes: 78 additions & 0 deletions index.d.ts
@@ -0,0 +1,78 @@
/* 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> {
multiArgs?: MultiArgs;
include?: Includes;
exclude?: Excludes;
errorFirst?: ErrorFirst;
}

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
? 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>>
>
: never;

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>
): PromisifyModule<Module, MultiArgs, ErrorFirst, Includes, Excludes>;

export = pify;
150 changes: 150 additions & 0 deletions index.test-d.ts
@@ -0,0 +1,150 @@
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<never>(pify((v: number) => {})());
expectType<never>(pify(() => 'hello')());
sindresorhus marked this conversation as resolved.
Show resolved Hide resolved

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

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

// Generics

declare function generic<T>(value: T, fn: (err: 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: (err: 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: (err: Error, value: number) => void): void;
declare function overloaded(value: string, cb: (err: 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<never>(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);

// errorFirst option:

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'),
);
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
}
}