Skip to content

Commit

Permalink
Meta tweaks
Browse files Browse the repository at this point in the history
  • Loading branch information
sindresorhus committed Sep 2, 2022
1 parent e13efc7 commit 7b70fd9
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 89 deletions.
43 changes: 21 additions & 22 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,41 @@
/* eslint-disable @typescript-eslint/ban-types */

type Last<T extends readonly unknown[]> = T extends [...any, infer L]
type LastArrayElement<T extends readonly unknown[]> = T extends [...any, infer L]
? L
: never;
type DropLast<T extends readonly unknown[]> = T extends [...(infer U), any]

type DropLastArrayElement<T extends readonly unknown[]> = T extends [...(infer U), unknown]
? 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> {
type 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> {
type 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>
...args: DropLastArrayElement<Args>
) =>
Last<Args> extends (...args: any) => any
LastArrayElement<Args> extends (...arguments_: 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>
? Parameters<LastArrayElement<Args>> extends [infer SingleCallbackArg] ? GenericOptions extends {errorFirst: true} ? Promise<unknown> : Promise<SingleCallbackArg>
: Promise<
GenericOptions extends {multiArgs: false}
? Last<Parameters<Last<Args>>>
: Parameters<Last<Args>>
? LastArrayElement<Parameters<LastArrayElement<Args>>>
: Parameters<LastArrayElement<Args>>
>
// Functions without a callback will return a promise that never settles. We model this as Promise<unknown>
: Promise<unknown>;
Expand All @@ -46,27 +47,27 @@ type PromisifyModule<
Includes extends ReadonlyArray<keyof Module>,
Excludes extends ReadonlyArray<keyof Module>,
> = {
[K in keyof Module]: Module[K] extends (...args: infer Args) => any
[K in keyof Module]: Module[K] extends (...arguments_: infer Arguments) => any
? K extends Includes[number]
? Promisify<Args, InternalOptions<Includes, Excludes, MultiArgs>>
? Promisify<Arguments, 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>>
: Promisify<Arguments, InternalOptions<Includes, Excludes, MultiArgs, ErrorFirst>>
: Module[K];
};

declare function pify<
FirstArg,
Args extends readonly unknown[],
export default function pify<
FirstArgument,
Arguments extends readonly unknown[],
MultiArgs extends boolean = false,
ErrorFirst extends boolean = true,
>(
input: (arg: FirstArg, ...args: Args) => any,
input: (argument: FirstArgument, ...arguments_: Arguments) => any,
options?: Options<[], [], MultiArgs, ErrorFirst>
): Promisify<[FirstArg, ...Args], InternalOptions<[], [], MultiArgs, ErrorFirst>>;
declare function pify<
): Promisify<[FirstArgument, ...Arguments], InternalOptions<[], [], MultiArgs, ErrorFirst>>;
export default function pify<
Module extends Record<string, any>,
Includes extends ReadonlyArray<keyof Module> = [],
Excludes extends ReadonlyArray<keyof Module> = [],
Expand All @@ -77,5 +78,3 @@ declare function pify<
module: Module,
options?: Options<Includes, Excludes, MultiArgs, ErrorFirst, true>
): PromisifyModule<Module, MultiArgs, ErrorFirst, Includes, Excludes>;

export = pify;
102 changes: 51 additions & 51 deletions index.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {expectError, expectType, printType} from 'tsd';
import pify from '.';
import pify from './index.js';

expectError(pify());
expectError(pify(null));
Expand All @@ -16,16 +16,16 @@ 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));
declare function function1(x: number, function_: (error: Error, value: number) => void): void;
expectType<Promise<number>>(pify(function1)(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));
declare function function2(x: number, y: number, function_: (error: Error, value: number) => void): void;
expectType<Promise<number>>(pify(function2)(1, 2));

// Generics

declare function generic<T>(value: T, fn: (error: Error, value: T) => void): void;
declare function generic<T>(value: T, function_: (error: Error, value: T) => void): void;
declare const genericValue: 'hello' | 'goodbye';
expectType<Promise<typeof genericValue>>(pify(generic)(genericValue));

Expand All @@ -41,40 +41,40 @@ declare function generic10<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(
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;
value1: T1;
value2: T2;
value3: T3;
value4: T4;
value5: T5;
value6: T6;
value7: T7;
value8: T8;
value9: T9;
value10: 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;
value1: 1;
value2: 2;
value3: 3;
value4: 4;
value5: 5;
value6: 6;
value7: 7;
value8: '8';
value9: 9;
value10: 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 callback02(callback: (x: number, y: string) => void): void;
declare function callback12(value: 'a', callback: (x: number, y: string) => void): void;
declare function callback22(
value1: 'a',
value2: 'b',
cb: (x: number, y: string) => void
callback: (x: number, y: string) => void
): void;

expectType<Promise<[number, string]>>(pify(callback02, {multiArgs: true})());
Expand All @@ -86,33 +86,33 @@ expectType<Promise<[number, string]>>(
);

// 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;
declare function overloaded(value: number, callback: (error: Error, value: number) => void): void;
declare function overloaded(value: string, callback: (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;
method1: (argument: string, callback: (error: Error, value: string) => void) => void;
method2: (argument: number, callback: (error: Error, value: number) => void) => void;
method3: (argument: string) => string;
methodSync: (argument: 'sync') => 'sync';
methodStream: (argument: 'stream') => 'stream';
callbackEndingInSync: (argument: 'sync', callback: (error: Error, value: 'sync') => void) => void;
property: number;
};

// Module support
expectType<number>(pify(fixtureModule).prop);
expectType<number>(pify(fixtureModule).property);
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
(argument: string, callback: (error: Error, value: string) => void) => void
>(pify(fixtureModule, {exclude: ['method1']}).method1);

// Includes
Expand All @@ -121,26 +121,26 @@ expectType<Promise<number>>(pify(fixtureModule, {include: ['method2']}).method2(

// Excludes sync and stream method by default
expectType<
(arg: 'sync') => 'sync'
(argument: 'sync') => 'sync'
>(pify(fixtureModule, {exclude: ['method1']}).methodSync);
expectType<
(arg: 'stream') => 'stream'
(argument: 'stream') => 'stream'
>(pify(fixtureModule, {exclude: ['method1']}).methodStream);

// Include sync method
expectType<
(arg: 'sync') => Promise<'sync'>
(argument: 'sync') => Promise<'sync'>
>(pify(fixtureModule, {include: ['callbackEndingInSync']}).callbackEndingInSync);

// Option errorFirst:

declare function fn0(fn: (value: number) => void): void;
declare function function0(function_: (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<unknown>>(pify(function0)());
expectType<Promise<unknown>>(pify(function0, {errorFirst: true})());

expectType<Promise<number>>(pify(fn0, {errorFirst: false})());
expectType<Promise<number>>(pify(function0, {errorFirst: false})());
expectType<Promise<[number, string]>>(pify(callback02, {multiArgs: true, errorFirst: true})());
expectType<Promise<[number, string]>>(
pify(callback12, {multiArgs: true, errorFirst: false})('a'),
Expand All @@ -152,9 +152,9 @@ expectType<Promise<[number, string]>>(
// Module function

// eslint-disable-next-line @typescript-eslint/no-empty-function
function moduleFunction(_cb: (error: Error, value: number) => void): void {}
function moduleFunction(_callback: (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 {};
moduleFunction.method = function (_callback: (error: Error, value: string) => void): void {};

expectType<Promise<number>>(pify(moduleFunction)());

Expand All @@ -163,8 +163,8 @@ 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;
method1(callback: (error: Error, value: string) => void): void;
method2(argument: number, callback: (error: Error, value: number) => void): void;
}

expectType<Promise<string>>(pify(new MyClass()).method1());
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
},
"type": "module",
"exports": "./index.js",
"types": "./index.d.ts",
"engines": {
"node": ">=14.16"
},
Expand Down Expand Up @@ -44,11 +45,10 @@
"bluebird"
],
"devDependencies": {
"ava": "^4.3.0",
"ava": "^4.3.3",
"pinkie-promise": "^2.0.1",
"tsd": "^0.23.0",
"typescript": "^4.8.2",
"v8-natives": "^1.2.5",
"xo": "^0.49.0"
"xo": "^0.52.3"
}
}
14 changes: 7 additions & 7 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,19 +142,19 @@ someClassPromisified.someFunction();
const someFunction = pify(someClass.someFunction.bind(someClass));
```

#### With TypeScript why is `pify` choosing the last function overload?
#### Why is `pify` choosing the last function overload when using it with TypeScript?

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'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.
If you need to choose a different overload, consider using a type assertion:

```ts
function overloadedFunction(input: number, cb: (error: unknown, data: number => void): void
function overloadedFunction(input: string, cb: (error: unknown, data: string) => void): void
/* ... */
function overloadedFunction(input: number, callback: (error: unknown, data: number => void): void
function overloadedFunction(input: string, callback: (error: unknown, data: string) => void): void {
/* */
}

const fn = pify(overloadedFunction as (input: number, cb: (error: unknown, data: number) => void) => void)
const fn = pify(overloadedFunction as (input: number, callback: (error: unknown, data: number) => void) => void)
// ^ ? (input: number) => Promise<number>
```
Expand Down
6 changes: 0 additions & 6 deletions tsconfig.json

This file was deleted.

0 comments on commit 7b70fd9

Please sign in to comment.