From 7a3a5590fded923be4f73ce41b8fee711cdd6476 Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Sat, 18 Dec 2021 15:09:59 +0000 Subject: [PATCH 01/16] WIP More obvious tests Generic af Saner tests WIP WIP --- index.d.ts | 30 ++++++++++++++++ index.test-d.ts | 96 +++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 7 ++-- tsconfig.json | 5 +++ 4 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 index.d.ts create mode 100644 index.test-d.ts create mode 100644 tsconfig.json diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..60d14b1 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,30 @@ +type Last = T extends [...any, infer L] + ? L + : never; +type DropLast = T extends [...(infer U), any] + ? U + : []; + +interface Options { + multiArgs: boolean; +} + +type Promisify = ( + ...args: DropLast +) => Last extends (...args: any) => any + ? Promise< + TOptions extends { multiArgs: false } + ? Last>> + : Parameters> + > + : never; + +declare function pify< + TArgs extends readonly unknown[], + TOptions extends Options = { multiArgs: false } +>( + input: (...args: TArgs) => any, + options?: TOptions +): Promisify; + +export = pify; diff --git a/index.test-d.ts b/index.test-d.ts new file mode 100644 index 0000000..d053ab3 --- /dev/null +++ b/index.test-d.ts @@ -0,0 +1,96 @@ +import { expectError, expectType } from "tsd"; +import pify = require("."); + +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", {})); + +expectType(pify((v: number) => {})()); + +// callback with 0 additional params +declare function fn0(fn: (val: number) => void): void; +expectType>(pify(fn0)()); + +// callback with 1 additional params +declare function fn1(x: number, fn: (val: number) => void): void; +expectType>(pify(fn1)(1)); + +// callback with 2 additional params +declare function fn2(x: number, y: number, fn: (val: number) => void): void; +expectType>(pify(fn2)(1, 2)); + +// generics + +declare function generic(val: T, fn: (val: T) => void): void; +declare const genericVal: "hello" | "goodbye"; +expectType>(pify(generic)(genericVal)); + +declare function generic10( + val1: T1, + val2: T2, + val3: T3, + val4: T4, + val5: T5, + val6: T6, + val7: T7, + val8: T8, + val9: T9, + val10: T10, + cb: (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(val: "a", cb: (x: number, y: string) => void): void; +declare function callback22( + val1: "a", + val2: "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: (value: number) => void): void; +declare function overloaded(value: string, cb: (value: string) => void): void; + +// Chooses last overload +// See https://github.com/microsoft/TypeScript/issues/32164 +expectType>(pify(overloaded)("")); diff --git a/package.json b/package.json index a5e5075..13f2d3b 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.19.0", + "typescript": "^4.5.4", "v8-natives": "^1.2.5", "xo": "^0.49.0" } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..aee0ec9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "strict": true + } +} From 4d76b8a567569bc0556fccf6bd6ce0cf7138ad27 Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Wed, 31 Aug 2022 10:29:25 +0100 Subject: [PATCH 02/16] Upgrade typescript and tsd --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 13f2d3b..62ad5d9 100644 --- a/package.json +++ b/package.json @@ -46,8 +46,8 @@ "devDependencies": { "ava": "^4.3.0", "pinkie-promise": "^2.0.1", - "tsd": "^0.19.0", - "typescript": "^4.5.4", + "tsd": "^0.23.0", + "typescript": "^4.8.2", "v8-natives": "^1.2.5", "xo": "^0.49.0" } From 43247764c4d7abf7c43961323461bba7d9b5956e Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Wed, 31 Aug 2022 10:50:17 +0100 Subject: [PATCH 03/16] pify modules --- index.d.ts | 11 +++++++++++ index.test-d.ts | 16 +++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 60d14b1..591c2c5 100644 --- a/index.d.ts +++ b/index.d.ts @@ -19,6 +19,10 @@ type Promisify = ( > : never; +type PromisifyModule = { + [K in keyof TModule]: TModule[K] extends (...args: infer TArgs) => any ? Promisify : never; +} + declare function pify< TArgs extends readonly unknown[], TOptions extends Options = { multiArgs: false } @@ -26,5 +30,12 @@ declare function pify< input: (...args: TArgs) => any, options?: TOptions ): Promisify; +declare function pify< + TModule extends { [key: string]: any }, + TOptions extends Options = { multiArgs: false } +>( + module: TModule, + options?: TOptions +): PromisifyModule; export = pify; diff --git a/index.test-d.ts b/index.test-d.ts index d053ab3..e152292 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1,4 +1,4 @@ -import { expectError, expectType } from "tsd"; +import { expectError, expectType, printType } from "tsd"; import pify = require("."); expectError(pify()); @@ -12,6 +12,8 @@ expectError(pify(123, {})); expectError(pify("abc", {})); expectType(pify((v: number) => {})()); +// TODO: Figure out a way for this to return `never` +expectType>(pify(() => 'hello')()); // callback with 0 additional params declare function fn0(fn: (val: number) => void): void; @@ -94,3 +96,15 @@ declare function overloaded(value: string, cb: (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 +} + +// module support +expectType>(pify(fixtureModule).method1("")); +expectType>(pify(fixtureModule).method2(0)); +// Same semantics as pify(fn) +expectType(pify(fixtureModule).method3()); From c0284d7bdcb5e694311dffa31c1b05d9d78f4b7e Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Wed, 31 Aug 2022 11:21:00 +0100 Subject: [PATCH 04/16] Support excludes --- index.d.ts | 42 +++++++++++++++++++++++++++++++----------- index.test-d.ts | 5 +++++ 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/index.d.ts b/index.d.ts index 591c2c5..034abfd 100644 --- a/index.d.ts +++ b/index.d.ts @@ -5,11 +5,19 @@ type DropLast = T extends [...(infer U), any] ? U : []; -interface Options { - multiArgs: boolean; +interface Options { + multiArgs?: TMultiArgs; + include?: TIncludes; + exclude?: TExcludes; } -type Promisify = ( +interface InternalOptions { + multiArgs: TMultiArgs; + include: TIncludes; + exclude: TExcludes; +} + +type Promisify> = ( ...args: DropLast ) => Last extends (...args: any) => any ? Promise< @@ -19,23 +27,35 @@ type Promisify = ( > : never; -type PromisifyModule = { - [K in keyof TModule]: TModule[K] extends (...args: infer TArgs) => any ? Promisify : never; +type PromisifyModule< + TModule extends { [key: string]: any }, + TMultiArgs extends boolean, + TIncludes extends ReadonlyArray, + TExcludes extends ReadonlyArray +> = { + [K in keyof TModule]: + TModule[K] extends (...args: infer TArgs) => any + ? K extends TExcludes[number] + ? TModule[K] + : Promisify> + : TModule[K] } declare function pify< TArgs extends readonly unknown[], - TOptions extends Options = { multiArgs: false } + TMultiArgs extends boolean = false, >( input: (...args: TArgs) => any, - options?: TOptions -): Promisify; + options?: Options<[], [], TMultiArgs> +): Promisify>; declare function pify< TModule extends { [key: string]: any }, - TOptions extends Options = { multiArgs: false } + TIncludes extends ReadonlyArray = [], + TExcludes extends ReadonlyArray = [], + TMultiArgs extends boolean = false, >( module: TModule, - options?: TOptions -): PromisifyModule; + options?: Options +): PromisifyModule; export = pify; diff --git a/index.test-d.ts b/index.test-d.ts index e152292..d272f7a 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -108,3 +108,8 @@ 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); From c30380bb8dcc3ab90b364478dc606f7ee21eeec0 Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Wed, 31 Aug 2022 13:04:32 +0100 Subject: [PATCH 05/16] Doesn't touch non-functions --- index.test-d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/index.test-d.ts b/index.test-d.ts index d272f7a..7a9cce3 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -101,9 +101,11 @@ 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 + prop: number; } // module support +expectType(pify(fixtureModule).prop); expectType>(pify(fixtureModule).method1("")); expectType>(pify(fixtureModule).method2(0)); // Same semantics as pify(fn) From fe79240d514ad5eb95681dc59d1d0f2759a2192f Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Wed, 31 Aug 2022 13:25:14 +0100 Subject: [PATCH 06/16] Includes and exclude sync/stream --- index.d.ts | 21 +++++++++++++-------- index.test-d.ts | 22 +++++++++++++++++++++- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/index.d.ts b/index.d.ts index 034abfd..b9383e7 100644 --- a/index.d.ts +++ b/index.d.ts @@ -5,6 +5,8 @@ type DropLast = T extends [...(infer U), any] ? U : []; +type StringEndsWith = S extends `${infer _}${X}` ? true : false; + interface Options { multiArgs?: TMultiArgs; include?: TIncludes; @@ -24,7 +26,7 @@ type Promisify>> : Parameters> - > + > : never; type PromisifyModule< @@ -33,13 +35,16 @@ type PromisifyModule< TIncludes extends ReadonlyArray, TExcludes extends ReadonlyArray > = { - [K in keyof TModule]: - TModule[K] extends (...args: infer TArgs) => any - ? K extends TExcludes[number] - ? TModule[K] - : Promisify> - : TModule[K] -} + [K in keyof TModule]: TModule[K] extends (...args: infer TArgs) => any + ? K extends TIncludes[number] + ? Promisify> + : K extends TExcludes[number] + ? TModule[K] + : StringEndsWith extends true + ? TModule[K] + : Promisify> + : TModule[K]; +}; declare function pify< TArgs extends readonly unknown[], diff --git a/index.test-d.ts b/index.test-d.ts index 7a9cce3..2f9c228 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -100,7 +100,10 @@ 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 + method3: (arg: string) => string; + methodSync: (arg: 'sync') => 'sync'; + methodStream: (arg: 'stream') => 'stream'; + callbackEndingInSync: (arg: 'sync', cb: (error: Error, value: 'sync') => void) => void; prop: number; } @@ -115,3 +118,20 @@ expectType(pify(fixtureModule).method3()); 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); From 530ca1a1a98afd9aa7d006f43160f77b7e14dae0 Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Wed, 31 Aug 2022 13:32:01 +0100 Subject: [PATCH 07/16] Fix linting --- index.d.ts | 81 +++++++++++++++--------------- index.test-d.ts | 129 ++++++++++++++++++++++++------------------------ tsconfig.json | 3 +- 3 files changed, 109 insertions(+), 104 deletions(-) diff --git a/index.d.ts b/index.d.ts index b9383e7..66c28d3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/ban-types */ + type Last = T extends [...any, infer L] ? L : never; @@ -7,60 +9,61 @@ type DropLast = T extends [...(infer U), any] type StringEndsWith = S extends `${infer _}${X}` ? true : false; -interface Options { - multiArgs?: TMultiArgs; - include?: TIncludes; - exclude?: TExcludes; +interface Options { + multiArgs?: MultiArgs; + include?: Includes; + exclude?: Excludes; } -interface InternalOptions { - multiArgs: TMultiArgs; - include: TIncludes; - exclude: TExcludes; +interface InternalOptions { + multiArgs: MultiArgs; + include: Includes; + exclude: Excludes; } -type Promisify> = ( - ...args: DropLast -) => Last extends (...args: any) => any +type Promisify> = ( + ...args: DropLast +) => Last extends (...args: any) => any ? Promise< - TOptions extends { multiArgs: false } - ? Last>> - : Parameters> - > + GenericOptions extends {multiArgs: false} + ? Last>> + : Parameters> + > : never; type PromisifyModule< - TModule extends { [key: string]: any }, - TMultiArgs extends boolean, - TIncludes extends ReadonlyArray, - TExcludes extends ReadonlyArray + Module extends Record, + MultiArgs extends boolean, + Includes extends ReadonlyArray, + Excludes extends ReadonlyArray, > = { - [K in keyof TModule]: TModule[K] extends (...args: infer TArgs) => any - ? K extends TIncludes[number] - ? Promisify> - : K extends TExcludes[number] - ? TModule[K] + [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 - ? TModule[K] - : Promisify> - : TModule[K]; + ? Module[K] + : Promisify> + : Module[K]; }; declare function pify< - TArgs extends readonly unknown[], - TMultiArgs extends boolean = false, + Args extends readonly unknown[], + MultiArgs extends boolean = false, >( - input: (...args: TArgs) => any, - options?: Options<[], [], TMultiArgs> -): Promisify>; + input: (...args: Args) => any, + options?: Options<[], [], MultiArgs> +): Promisify>; declare function pify< - TModule extends { [key: string]: any }, - TIncludes extends ReadonlyArray = [], - TExcludes extends ReadonlyArray = [], - TMultiArgs extends boolean = false, + Module extends Record, + Includes extends ReadonlyArray = [], + Excludes extends ReadonlyArray = [], + MultiArgs extends boolean = false, >( - module: TModule, - options?: Options -): PromisifyModule; + // 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 index 2f9c228..80ae70f 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1,49 +1,50 @@ -import { expectError, expectType, printType } from "tsd"; -import pify = require("."); +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('abc')); expectError(pify(null, {})); expectError(pify(undefined, {})); expectError(pify(123, {})); -expectError(pify("abc", {})); +expectError(pify('abc', {})); +// eslint-disable-next-line @typescript-eslint/no-empty-function expectType(pify((v: number) => {})()); // TODO: Figure out a way for this to return `never` expectType>(pify(() => 'hello')()); -// callback with 0 additional params -declare function fn0(fn: (val: number) => void): void; +// Callback with 0 additional params +declare function fn0(fn: (value: number) => void): void; expectType>(pify(fn0)()); -// callback with 1 additional params -declare function fn1(x: number, fn: (val: number) => void): void; +// Callback with 1 additional params +declare function fn1(x: number, fn: (value: number) => void): void; expectType>(pify(fn1)(1)); -// callback with 2 additional params -declare function fn2(x: number, y: number, fn: (val: number) => void): void; +// Callback with 2 additional params +declare function fn2(x: number, y: number, fn: (value: number) => void): void; expectType>(pify(fn2)(1, 2)); -// generics +// Generics -declare function generic(val: T, fn: (val: T) => void): void; -declare const genericVal: "hello" | "goodbye"; -expectType>(pify(generic)(genericVal)); +declare function generic(value: T, fn: (value: T) => void): void; +declare const genericValue: 'hello' | 'goodbye'; +expectType>(pify(generic)(genericValue)); declare function generic10( - val1: T1, - val2: T2, - val3: T3, - val4: T4, - val5: T5, - val6: T6, - val7: T7, - val8: T8, - val9: T9, - val10: T10, + value1: T1, + value2: T2, + value3: T3, + value4: T4, + value5: T5, + value6: T6, + value7: T7, + value8: T8, + value9: T9, + value10: T10, cb: (value: { val1: T1; val2: T2; @@ -58,44 +59,44 @@ declare function generic10( }) => 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 +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(val: "a", cb: (x: number, y: string) => void): void; +declare function callback12(value: 'a', cb: (x: number, y: string) => void): void; declare function callback22( - val1: "a", - val2: "b", + value1: 'a', + value2: 'b', cb: (x: number, y: string) => void ): void; -expectType>(pify(callback02, { multiArgs: true })()); +expectType>(pify(callback02, {multiArgs: true})()); expectType>( - pify(callback12, { multiArgs: true })("a") + pify(callback12, {multiArgs: true})('a'), ); expectType>( - pify(callback22, { multiArgs: true })("a", "b") + pify(callback22, {multiArgs: true})('a', 'b'), ); -// overloads +// Overloads declare function overloaded(value: number, cb: (value: number) => void): void; declare function overloaded(value: string, cb: (value: string) => void): void; // Chooses last overload // See https://github.com/microsoft/TypeScript/issues/32164 -expectType>(pify(overloaded)("")); +expectType>(pify(overloaded)('')); declare const fixtureModule: { method1: (arg: string, cb: (error: Error, value: string) => void) => void; @@ -105,33 +106,33 @@ declare const fixtureModule: { methodStream: (arg: 'stream') => 'stream'; callbackEndingInSync: (arg: 'sync', cb: (error: Error, value: 'sync') => void) => void; prop: number; -} +}; -// module support +// Module support expectType(pify(fixtureModule).prop); -expectType>(pify(fixtureModule).method1("")); +expectType>(pify(fixtureModule).method1('')); expectType>(pify(fixtureModule).method2(0)); // Same semantics as pify(fn) expectType(pify(fixtureModule).method3()); -// excludes +// Excludes expectType< - (arg: string, cb: (error: Error, value: string) => void) => void ->(pify(fixtureModule, { exclude: ['method1'] }).method1); +(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)); +// Includes +expectType>(pify(fixtureModule, {include: ['method1']}).method1('')); +expectType>(pify(fixtureModule, {include: ['method2']}).method2(0)); -// excludes sync and stream method by default +// Excludes sync and stream method by default expectType< - (arg: 'sync') => 'sync' ->(pify(fixtureModule, { exclude: ['method1'] }).methodSync); +(arg: 'sync') => 'sync' +>(pify(fixtureModule, {exclude: ['method1']}).methodSync); expectType< - (arg: 'stream') => 'stream' ->(pify(fixtureModule, { exclude: ['method1'] }).methodStream); +(arg: 'stream') => 'stream' +>(pify(fixtureModule, {exclude: ['method1']}).methodStream); -// include sync method +// Include sync method expectType< - (arg: 'sync') => Promise<'sync'> ->(pify(fixtureModule, { include: ['callbackEndingInSync'] }).callbackEndingInSync); +(arg: 'sync') => Promise<'sync'> +>(pify(fixtureModule, {include: ['callbackEndingInSync']}).callbackEndingInSync); diff --git a/tsconfig.json b/tsconfig.json index aee0ec9..75cf960 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { - "strict": true + "strict": true, + "esModuleInterop": true } } From f0817c8d44c8eca3d60162f719b9d61d8ba551ba Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Wed, 31 Aug 2022 13:58:05 +0100 Subject: [PATCH 08/16] Resolve todo --- index.d.ts | 5 +++-- index.test-d.ts | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/index.d.ts b/index.d.ts index 66c28d3..30823ef 100644 --- a/index.d.ts +++ b/index.d.ts @@ -49,12 +49,13 @@ type PromisifyModule< }; declare function pify< + FirstArg, Args extends readonly unknown[], MultiArgs extends boolean = false, >( - input: (...args: Args) => any, + input: (arg: FirstArg, ...args: Args) => any, options?: Options<[], [], MultiArgs> -): Promisify>; +): Promisify<[FirstArg, ...Args], InternalOptions<[], [], MultiArgs>>; declare function pify< Module extends Record, Includes extends ReadonlyArray = [], diff --git a/index.test-d.ts b/index.test-d.ts index 80ae70f..71e05fc 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -13,8 +13,7 @@ expectError(pify('abc', {})); // eslint-disable-next-line @typescript-eslint/no-empty-function expectType(pify((v: number) => {})()); -// TODO: Figure out a way for this to return `never` -expectType>(pify(() => 'hello')()); +expectType(pify(() => 'hello')()); // Callback with 0 additional params declare function fn0(fn: (value: number) => void): void; From 75c9246e86b299e6f14be9a65edaced1c07d5fe6 Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Wed, 31 Aug 2022 14:13:11 +0100 Subject: [PATCH 09/16] Add note on function overloads --- readme.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/readme.md b/readme.md index 76becda..a1b654c 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 first 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 first 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: string, cb: (error: unknown, data: string) => void) => void) +// ^ ? (input: string) => Promise +``` + ## Related - [p-event](https://github.com/sindresorhus/p-event) - Promisify an event by waiting for it to be emitted From c06efd9561ac40080f061c53ca8bf60a630e8b39 Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Wed, 31 Aug 2022 15:10:42 +0100 Subject: [PATCH 10/16] typo --- readme.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/readme.md b/readme.md index a1b654c..adefb02 100644 --- a/readme.md +++ b/readme.md @@ -142,9 +142,9 @@ someClassPromisified.someFunction(); const someFunction = pify(someClass.someFunction.bind(someClass)); ``` -#### With TypeScript why is `pify` choosing the first function overload? +#### 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 first overload will be chosen and promisified. +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. @@ -154,8 +154,8 @@ function overloadedFunction(input: string, cb: (error: unknown, data: string) => /* ... */ } -const fn = pify(overloadedFunction as (input: string, cb: (error: unknown, data: string) => void) => void) -// ^ ? (input: string) => Promise +const fn = pify(overloadedFunction as (input: number, cb: (error: unknown, data: number) => void) => void) +// ^ ? (input: number) => Promise ``` ## Related From da65e27e84f72403710758b75c80e0ab63ddd37d Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Wed, 31 Aug 2022 15:31:12 +0100 Subject: [PATCH 11/16] Add errorFirst support --- index.d.ts | 28 ++++++++++++++++++---------- index.test-d.ts | 33 +++++++++++++++++++++++---------- 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/index.d.ts b/index.d.ts index 30823ef..e0b3772 100644 --- a/index.d.ts +++ b/index.d.ts @@ -9,22 +9,27 @@ type DropLast = T extends [...(infer U), any] type StringEndsWith = S extends `${infer _}${X}` ? true : false; -interface Options { +interface Options { multiArgs?: MultiArgs; include?: Includes; exclude?: Excludes; + errorFirst?: ErrorFirst; } -interface InternalOptions { +interface InternalOptions { multiArgs: MultiArgs; include: Includes; exclude: Excludes; + errorFirst: ErrorFirst; } -type Promisify> = ( + +type Promisify> = ( ...args: DropLast -) => Last extends (...args: any) => any - ? Promise< +) => +Last extends (...args: any) => any + ? Parameters> extends [infer SingleCallbackArg] ? GenericOptions extends { errorFirst: true } ? Promise : Promise + : Promise< GenericOptions extends {multiArgs: false} ? Last>> : Parameters> @@ -34,6 +39,7 @@ type Promisify, MultiArgs extends boolean, + ErrorFirst extends boolean, Includes extends ReadonlyArray, Excludes extends ReadonlyArray, > = { @@ -44,7 +50,7 @@ type PromisifyModule< ? Module[K] : StringEndsWith extends true ? Module[K] - : Promisify> + : Promisify> : Module[K]; }; @@ -52,19 +58,21 @@ 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> -): Promisify<[FirstArg, ...Args], InternalOptions<[], [], MultiArgs>>; + 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; + options?: Options +): PromisifyModule; export = pify; diff --git a/index.test-d.ts b/index.test-d.ts index 71e05fc..88ee49a 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -15,21 +15,17 @@ expectError(pify('abc', {})); expectType(pify((v: number) => {})()); expectType(pify(() => 'hello')()); -// Callback with 0 additional params -declare function fn0(fn: (value: number) => void): void; -expectType>(pify(fn0)()); - // Callback with 1 additional params -declare function fn1(x: number, fn: (value: number) => void): void; +declare function fn1(x: number, fn: (err: Error, value: number) => void): void; expectType>(pify(fn1)(1)); // Callback with 2 additional params -declare function fn2(x: number, y: number, fn: (value: number) => void): void; +declare function fn2(x: number, y: number, fn: (err: Error, value: number) => void): void; expectType>(pify(fn2)(1, 2)); // Generics -declare function generic(value: T, fn: (value: T) => void): void; +declare function generic(value: T, fn: (err: Error, value: T) => void): void; declare const genericValue: 'hello' | 'goodbye'; expectType>(pify(generic)(genericValue)); @@ -44,7 +40,7 @@ declare function generic10( value8: T8, value9: T9, value10: T10, - cb: (value: { + cb: (err: Error, value: { val1: T1; val2: T2; val3: T3; @@ -90,8 +86,8 @@ expectType>( ); // Overloads -declare function overloaded(value: number, cb: (value: number) => void): void; -declare function overloaded(value: string, cb: (value: string) => void): void; +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 @@ -135,3 +131,20 @@ expectType< 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>(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'), +); From 1756abb2a4c1e62cfd9591724a47962f0bd83117 Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Wed, 31 Aug 2022 16:00:30 +0100 Subject: [PATCH 12/16] Add promiseModule option --- index.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/index.d.ts b/index.d.ts index e0b3772..7cd4641 100644 --- a/index.d.ts +++ b/index.d.ts @@ -14,6 +14,7 @@ interface Options { @@ -28,6 +29,7 @@ type Promisify ) => 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} From 912bfad9da95e25e956b1e8fd817eb77ba79d445 Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Wed, 31 Aug 2022 16:02:24 +0100 Subject: [PATCH 13/16] Fix lints --- index.d.ts | 15 +++++++-------- index.test-d.ts | 18 +++++++++--------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/index.d.ts b/index.d.ts index 7cd4641..9a97716 100644 --- a/index.d.ts +++ b/index.d.ts @@ -24,18 +24,17 @@ interface InternalOptions> = ( ...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> - > +// 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> + > : never; type PromisifyModule< diff --git a/index.test-d.ts b/index.test-d.ts index 88ee49a..f25c1c5 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -16,16 +16,16 @@ expectType(pify((v: number) => {})()); expectType(pify(() => 'hello')()); // Callback with 1 additional params -declare function fn1(x: number, fn: (err: Error, value: number) => void): void; +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: (err: Error, value: number) => void): void; +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: (err: Error, value: T) => void): void; +declare function generic(value: T, fn: (error: Error, value: T) => void): void; declare const genericValue: 'hello' | 'goodbye'; expectType>(pify(generic)(genericValue)); @@ -40,7 +40,7 @@ declare function generic10( value8: T8, value9: T9, value10: T10, - cb: (err: Error, value: { + cb: (error: Error, value: { val1: T1; val2: T2; val3: T3; @@ -86,8 +86,8 @@ expectType>( ); // 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; +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 @@ -132,15 +132,15 @@ expectType< (arg: 'sync') => Promise<'sync'> >(pify(fixtureModule, {include: ['callbackEndingInSync']}).callbackEndingInSync); -// errorFirst option: +// 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: true})()); -expectType>(pify(fn0, { errorFirst: false })()); +expectType>(pify(fn0, {errorFirst: false})()); expectType>(pify(callback02, {multiArgs: true, errorFirst: true})()); expectType>( pify(callback12, {multiArgs: true, errorFirst: false})('a'), From 4987750dc53d9913a0dc627ed37ec5e304ebdacb Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Wed, 31 Aug 2022 17:22:36 +0100 Subject: [PATCH 14/16] Better function modules support When excludeMain is false (default) we pify the main function When it's true, we pify the module members. There is unfortunately no way of promisifying both. --- index.d.ts | 5 +++-- index.test-d.ts | 11 +++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/index.d.ts b/index.d.ts index 9a97716..d65565f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -9,12 +9,13 @@ type DropLast = T extends [...(infer U), any] type StringEndsWith = S extends `${infer _}${X}` ? true : false; -interface Options { +interface Options { multiArgs?: MultiArgs; include?: Includes; exclude?: Excludes; errorFirst?: ErrorFirst; promiseModule?: PromiseConstructor; + excludeMain?: ExcludeMain; } interface InternalOptions { @@ -73,7 +74,7 @@ declare function pify< >( // eslint-disable-next-line unicorn/prefer-module module: Module, - options?: Options + options?: Options ): PromisifyModule; export = pify; diff --git a/index.test-d.ts b/index.test-d.ts index f25c1c5..b44a4e5 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -148,3 +148,14 @@ expectType>( 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()); From 76e621d163d80f8d3621bb8c5f7180a42b5e02e4 Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Wed, 31 Aug 2022 17:33:25 +0100 Subject: [PATCH 15/16] Add class test --- index.test-d.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/index.test-d.ts b/index.test-d.ts index b44a4e5..42fead7 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -159,3 +159,13 @@ moduleFunction.method = function (_cb: (error: Error, value: string) => void): v 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)); From 479654c179fc94e5ca0e763129965c36d635af8c Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Wed, 31 Aug 2022 18:17:02 +0100 Subject: [PATCH 16/16] Return Promise for functions without a callback --- index.d.ts | 3 ++- index.test-d.ts | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/index.d.ts b/index.d.ts index d65565f..5f6c633 100644 --- a/index.d.ts +++ b/index.d.ts @@ -36,7 +36,8 @@ Last extends (...args: any) => any ? Last>> : Parameters> > - : never; + // Functions without a callback will return a promise that never settles. We model this as Promise + : Promise; type PromisifyModule< Module extends Record, diff --git a/index.test-d.ts b/index.test-d.ts index 42fead7..4268720 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -12,8 +12,8 @@ expectError(pify(123, {})); expectError(pify('abc', {})); // eslint-disable-next-line @typescript-eslint/no-empty-function -expectType(pify((v: number) => {})()); -expectType(pify(() => 'hello')()); +expectType>(pify((v: number) => {})()); +expectType>(pify(() => 'hello')()); // Callback with 1 additional params declare function fn1(x: number, fn: (error: Error, value: number) => void): void; @@ -108,7 +108,7 @@ expectType(pify(fixtureModule).prop); expectType>(pify(fixtureModule).method1('')); expectType>(pify(fixtureModule).method2(0)); // Same semantics as pify(fn) -expectType(pify(fixtureModule).method3()); +expectType>(pify(fixtureModule).method3()); // Excludes expectType<