Skip to content

Commit e13efc7

Browse files
authoredSep 2, 2022
Add TypeScript definitions (#86)
1 parent 513b0d5 commit e13efc7

File tree

5 files changed

+279
-2
lines changed

5 files changed

+279
-2
lines changed
 

‎index.d.ts

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/* eslint-disable @typescript-eslint/ban-types */
2+
3+
type Last<T extends readonly unknown[]> = T extends [...any, infer L]
4+
? L
5+
: never;
6+
type DropLast<T extends readonly unknown[]> = T extends [...(infer U), any]
7+
? U
8+
: [];
9+
10+
type StringEndsWith<S, X extends string> = S extends `${infer _}${X}` ? true : false;
11+
12+
interface Options<Includes extends readonly unknown[], Excludes extends readonly unknown[], MultiArgs extends boolean = false, ErrorFirst extends boolean = true, ExcludeMain extends boolean = false> {
13+
multiArgs?: MultiArgs;
14+
include?: Includes;
15+
exclude?: Excludes;
16+
errorFirst?: ErrorFirst;
17+
promiseModule?: PromiseConstructor;
18+
excludeMain?: ExcludeMain;
19+
}
20+
21+
interface InternalOptions<Includes extends readonly unknown[], Excludes extends readonly unknown[], MultiArgs extends boolean = false, ErrorFirst extends boolean = true> {
22+
multiArgs: MultiArgs;
23+
include: Includes;
24+
exclude: Excludes;
25+
errorFirst: ErrorFirst;
26+
}
27+
28+
type Promisify<Args extends readonly unknown[], GenericOptions extends InternalOptions<readonly unknown[], readonly unknown[], boolean, boolean>> = (
29+
...args: DropLast<Args>
30+
) =>
31+
Last<Args> extends (...args: any) => any
32+
// For single-argument functions when errorFirst: true we just return Promise<unknown> as it will always reject.
33+
? Parameters<Last<Args>> extends [infer SingleCallbackArg] ? GenericOptions extends {errorFirst: true} ? Promise<unknown> : Promise<SingleCallbackArg>
34+
: Promise<
35+
GenericOptions extends {multiArgs: false}
36+
? Last<Parameters<Last<Args>>>
37+
: Parameters<Last<Args>>
38+
>
39+
// Functions without a callback will return a promise that never settles. We model this as Promise<unknown>
40+
: Promise<unknown>;
41+
42+
type PromisifyModule<
43+
Module extends Record<string, any>,
44+
MultiArgs extends boolean,
45+
ErrorFirst extends boolean,
46+
Includes extends ReadonlyArray<keyof Module>,
47+
Excludes extends ReadonlyArray<keyof Module>,
48+
> = {
49+
[K in keyof Module]: Module[K] extends (...args: infer Args) => any
50+
? K extends Includes[number]
51+
? Promisify<Args, InternalOptions<Includes, Excludes, MultiArgs>>
52+
: K extends Excludes[number]
53+
? Module[K]
54+
: StringEndsWith<K, 'Sync' | 'Stream'> extends true
55+
? Module[K]
56+
: Promisify<Args, InternalOptions<Includes, Excludes, MultiArgs, ErrorFirst>>
57+
: Module[K];
58+
};
59+
60+
declare function pify<
61+
FirstArg,
62+
Args extends readonly unknown[],
63+
MultiArgs extends boolean = false,
64+
ErrorFirst extends boolean = true,
65+
>(
66+
input: (arg: FirstArg, ...args: Args) => any,
67+
options?: Options<[], [], MultiArgs, ErrorFirst>
68+
): Promisify<[FirstArg, ...Args], InternalOptions<[], [], MultiArgs, ErrorFirst>>;
69+
declare function pify<
70+
Module extends Record<string, any>,
71+
Includes extends ReadonlyArray<keyof Module> = [],
72+
Excludes extends ReadonlyArray<keyof Module> = [],
73+
MultiArgs extends boolean = false,
74+
ErrorFirst extends boolean = true,
75+
>(
76+
// eslint-disable-next-line unicorn/prefer-module
77+
module: Module,
78+
options?: Options<Includes, Excludes, MultiArgs, ErrorFirst, true>
79+
): PromisifyModule<Module, MultiArgs, ErrorFirst, Includes, Excludes>;
80+
81+
export = pify;

‎index.test-d.ts

+171
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import {expectError, expectType, printType} from 'tsd';
2+
import pify from '.';
3+
4+
expectError(pify());
5+
expectError(pify(null));
6+
expectError(pify(undefined));
7+
expectError(pify(123));
8+
expectError(pify('abc'));
9+
expectError(pify(null, {}));
10+
expectError(pify(undefined, {}));
11+
expectError(pify(123, {}));
12+
expectError(pify('abc', {}));
13+
14+
// eslint-disable-next-line @typescript-eslint/no-empty-function
15+
expectType<Promise<unknown>>(pify((v: number) => {})());
16+
expectType<Promise<unknown>>(pify(() => 'hello')());
17+
18+
// Callback with 1 additional params
19+
declare function fn1(x: number, fn: (error: Error, value: number) => void): void;
20+
expectType<Promise<number>>(pify(fn1)(1));
21+
22+
// Callback with 2 additional params
23+
declare function fn2(x: number, y: number, fn: (error: Error, value: number) => void): void;
24+
expectType<Promise<number>>(pify(fn2)(1, 2));
25+
26+
// Generics
27+
28+
declare function generic<T>(value: T, fn: (error: Error, value: T) => void): void;
29+
declare const genericValue: 'hello' | 'goodbye';
30+
expectType<Promise<typeof genericValue>>(pify(generic)(genericValue));
31+
32+
declare function generic10<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(
33+
value1: T1,
34+
value2: T2,
35+
value3: T3,
36+
value4: T4,
37+
value5: T5,
38+
value6: T6,
39+
value7: T7,
40+
value8: T8,
41+
value9: T9,
42+
value10: T10,
43+
cb: (error: Error, value: {
44+
val1: T1;
45+
val2: T2;
46+
val3: T3;
47+
val4: T4;
48+
val5: T5;
49+
val6: T6;
50+
val7: T7;
51+
val8: T8;
52+
val9: T9;
53+
val10: T10;
54+
}) => void
55+
): void;
56+
expectType<
57+
Promise<{
58+
val1: 1;
59+
val2: 2;
60+
val3: 3;
61+
val4: 4;
62+
val5: 5;
63+
val6: 6;
64+
val7: 7;
65+
val8: '8';
66+
val9: 9;
67+
val10: 10;
68+
}>
69+
>(pify(generic10)(1, 2, 3, 4, 5, 6, 7, '8', 9, 10));
70+
71+
// MultiArgs
72+
declare function callback02(cb: (x: number, y: string) => void): void;
73+
declare function callback12(value: 'a', cb: (x: number, y: string) => void): void;
74+
declare function callback22(
75+
value1: 'a',
76+
value2: 'b',
77+
cb: (x: number, y: string) => void
78+
): void;
79+
80+
expectType<Promise<[number, string]>>(pify(callback02, {multiArgs: true})());
81+
expectType<Promise<[number, string]>>(
82+
pify(callback12, {multiArgs: true})('a'),
83+
);
84+
expectType<Promise<[number, string]>>(
85+
pify(callback22, {multiArgs: true})('a', 'b'),
86+
);
87+
88+
// Overloads
89+
declare function overloaded(value: number, cb: (error: Error, value: number) => void): void;
90+
declare function overloaded(value: string, cb: (error: Error, value: string) => void): void;
91+
92+
// Chooses last overload
93+
// See https://github.com/microsoft/TypeScript/issues/32164
94+
expectType<Promise<string>>(pify(overloaded)(''));
95+
96+
declare const fixtureModule: {
97+
method1: (arg: string, cb: (error: Error, value: string) => void) => void;
98+
method2: (arg: number, cb: (error: Error, value: number) => void) => void;
99+
method3: (arg: string) => string;
100+
methodSync: (arg: 'sync') => 'sync';
101+
methodStream: (arg: 'stream') => 'stream';
102+
callbackEndingInSync: (arg: 'sync', cb: (error: Error, value: 'sync') => void) => void;
103+
prop: number;
104+
};
105+
106+
// Module support
107+
expectType<number>(pify(fixtureModule).prop);
108+
expectType<Promise<string>>(pify(fixtureModule).method1(''));
109+
expectType<Promise<number>>(pify(fixtureModule).method2(0));
110+
// Same semantics as pify(fn)
111+
expectType<Promise<unknown>>(pify(fixtureModule).method3());
112+
113+
// Excludes
114+
expectType<
115+
(arg: string, cb: (error: Error, value: string) => void) => void
116+
>(pify(fixtureModule, {exclude: ['method1']}).method1);
117+
118+
// Includes
119+
expectType<Promise<string>>(pify(fixtureModule, {include: ['method1']}).method1(''));
120+
expectType<Promise<number>>(pify(fixtureModule, {include: ['method2']}).method2(0));
121+
122+
// Excludes sync and stream method by default
123+
expectType<
124+
(arg: 'sync') => 'sync'
125+
>(pify(fixtureModule, {exclude: ['method1']}).methodSync);
126+
expectType<
127+
(arg: 'stream') => 'stream'
128+
>(pify(fixtureModule, {exclude: ['method1']}).methodStream);
129+
130+
// Include sync method
131+
expectType<
132+
(arg: 'sync') => Promise<'sync'>
133+
>(pify(fixtureModule, {include: ['callbackEndingInSync']}).callbackEndingInSync);
134+
135+
// Option errorFirst:
136+
137+
declare function fn0(fn: (value: number) => void): void;
138+
139+
// Unknown as it returns a promise that always rejects because errorFirst = true
140+
expectType<Promise<unknown>>(pify(fn0)());
141+
expectType<Promise<unknown>>(pify(fn0, {errorFirst: true})());
142+
143+
expectType<Promise<number>>(pify(fn0, {errorFirst: false})());
144+
expectType<Promise<[number, string]>>(pify(callback02, {multiArgs: true, errorFirst: true})());
145+
expectType<Promise<[number, string]>>(
146+
pify(callback12, {multiArgs: true, errorFirst: false})('a'),
147+
);
148+
expectType<Promise<[number, string]>>(
149+
pify(callback22, {multiArgs: true, errorFirst: false})('a', 'b'),
150+
);
151+
152+
// Module function
153+
154+
// eslint-disable-next-line @typescript-eslint/no-empty-function
155+
function moduleFunction(_cb: (error: Error, value: number) => void): void {}
156+
// eslint-disable-next-line @typescript-eslint/no-empty-function
157+
moduleFunction.method = function (_cb: (error: Error, value: string) => void): void {};
158+
159+
expectType<Promise<number>>(pify(moduleFunction)());
160+
161+
expectType<Promise<string>>(pify(moduleFunction, {excludeMain: true}).method());
162+
163+
// Classes
164+
165+
declare class MyClass {
166+
method1(cb: (error: Error, value: string) => void): void;
167+
method2(arg: number, cb: (error: Error, value: number) => void): void;
168+
}
169+
170+
expectType<Promise<string>>(pify(new MyClass()).method1());
171+
expectType<Promise<number>>(pify(new MyClass()).method2(4));

‎package.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@
1616
"node": ">=14.16"
1717
},
1818
"scripts": {
19-
"test": "xo && ava",
19+
"test": "xo && ava && tsd",
2020
"optimization-test": "node --allow-natives-syntax optimization-test.js"
2121
},
2222
"files": [
23-
"index.js"
23+
"index.js",
24+
"index.d.ts"
2425
],
2526
"keywords": [
2627
"promisify",
@@ -45,6 +46,8 @@
4546
"devDependencies": {
4647
"ava": "^4.3.0",
4748
"pinkie-promise": "^2.0.1",
49+
"tsd": "^0.23.0",
50+
"typescript": "^4.8.2",
4851
"v8-natives": "^1.2.5",
4952
"xo": "^0.49.0"
5053
}

‎readme.md

+16
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,22 @@ someClassPromisified.someFunction();
142142
const someFunction = pify(someClass.someFunction.bind(someClass));
143143
```
144144

145+
#### With TypeScript why is `pify` choosing the last function overload?
146+
147+
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.
148+
149+
If you need to choose a different overload consider using a type assertion eg.
150+
151+
```ts
152+
function overloadedFunction(input: number, cb: (error: unknown, data: number => void): void
153+
function overloadedFunction(input: string, cb: (error: unknown, data: string) => void): void
154+
/* ... */
155+
}
156+
157+
const fn = pify(overloadedFunction as (input: number, cb: (error: unknown, data: number) => void) => void)
158+
// ^ ? (input: number) => Promise<number>
159+
```
160+
145161
## Related
146162
147163
- [p-event](https://github.com/sindresorhus/p-event) - Promisify an event by waiting for it to be emitted

‎tsconfig.json

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"compilerOptions": {
3+
"strict": true,
4+
"esModuleInterop": true
5+
}
6+
}

0 commit comments

Comments
 (0)
Please sign in to comment.