diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..5f6c633 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,81 @@ +/* eslint-disable @typescript-eslint/ban-types */ + +type Last = T extends [...any, infer L] + ? L + : never; +type DropLast = T extends [...(infer U), any] + ? U + : []; + +type StringEndsWith = S extends `${infer _}${X}` ? true : false; + +interface Options { + multiArgs?: MultiArgs; + include?: Includes; + exclude?: Excludes; + errorFirst?: ErrorFirst; + promiseModule?: PromiseConstructor; + excludeMain?: ExcludeMain; +} + +interface InternalOptions { + multiArgs: MultiArgs; + include: Includes; + exclude: Excludes; + errorFirst: ErrorFirst; +} + +type Promisify> = ( + ...args: DropLast +) => +Last extends (...args: any) => any +// For single-argument functions when errorFirst: true we just return Promise as it will always reject. + ? Parameters> extends [infer SingleCallbackArg] ? GenericOptions extends {errorFirst: true} ? Promise : Promise + : Promise< + GenericOptions extends {multiArgs: false} + ? Last>> + : Parameters> + > + // Functions without a callback will return a promise that never settles. We model this as Promise + : Promise; + +type PromisifyModule< + Module extends Record, + MultiArgs extends boolean, + ErrorFirst extends boolean, + Includes extends ReadonlyArray, + Excludes extends ReadonlyArray, +> = { + [K in keyof Module]: Module[K] extends (...args: infer Args) => any + ? K extends Includes[number] + ? Promisify> + : K extends Excludes[number] + ? Module[K] + : StringEndsWith extends true + ? Module[K] + : Promisify> + : 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, + Includes extends ReadonlyArray = [], + Excludes extends ReadonlyArray = [], + MultiArgs extends boolean = false, + ErrorFirst extends boolean = true, +>( + // eslint-disable-next-line unicorn/prefer-module + module: Module, + options?: Options +): PromisifyModule; + +export = pify; diff --git a/index.test-d.ts b/index.test-d.ts new file mode 100644 index 0000000..4268720 --- /dev/null +++ b/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>(pify((v: number) => {})()); +expectType>(pify(() => 'hello')()); + +// Callback with 1 additional params +declare function fn1(x: number, fn: (error: Error, value: number) => void): void; +expectType>(pify(fn1)(1)); + +// Callback with 2 additional params +declare function fn2(x: number, y: number, fn: (error: Error, value: number) => void): void; +expectType>(pify(fn2)(1, 2)); + +// Generics + +declare function generic(value: T, fn: (error: Error, value: T) => void): void; +declare const genericValue: 'hello' | 'goodbye'; +expectType>(pify(generic)(genericValue)); + +declare function generic10( + 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>(pify(callback02, {multiArgs: true})()); +expectType>( + pify(callback12, {multiArgs: true})('a'), +); +expectType>( + 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>(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(pify(fixtureModule).prop); +expectType>(pify(fixtureModule).method1('')); +expectType>(pify(fixtureModule).method2(0)); +// Same semantics as pify(fn) +expectType>(pify(fixtureModule).method3()); + +// Excludes +expectType< +(arg: string, cb: (error: Error, value: string) => void) => void +>(pify(fixtureModule, {exclude: ['method1']}).method1); + +// Includes +expectType>(pify(fixtureModule, {include: ['method1']}).method1('')); +expectType>(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>(pify(fn0)()); +expectType>(pify(fn0, {errorFirst: true})()); + +expectType>(pify(fn0, {errorFirst: false})()); +expectType>(pify(callback02, {multiArgs: true, errorFirst: true})()); +expectType>( + pify(callback12, {multiArgs: true, errorFirst: false})('a'), +); +expectType>( + 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>(pify(moduleFunction)()); + +expectType>(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>(pify(new MyClass()).method1()); +expectType>(pify(new MyClass()).method2(4)); diff --git a/package.json b/package.json index a5e5075..62ad5d9 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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" } diff --git a/readme.md b/readme.md index 76becda..adefb02 100644 --- a/readme.md +++ b/readme.md @@ -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 +``` + ## Related - [p-event](https://github.com/sindresorhus/p-event) - Promisify an event by waiting for it to be emitted diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..75cf960 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true + } +}