Description
Now that #26063 is merged (which is great BTW), I was toying around with an alternate solution to #1360 or #25717. The basic idea is to take the parameter list of a NodeJS callback style API as a tuple and generate two other types from it:
1. The type of the last item in the tuple, which can currently be achieved doing this:
// Helper type to drop the first item in a tuple, i.e. reduce its size by 1
type Drop1<T extends any[]> = ((...args: T) => void) extends ((a1: any, ...rest: infer R) => void) ? R : never;
type TakeLast<
T extends any[],
// Create a tuple which is 1 item shorter than T and determine its length
L1 extends number = Drop1<T>["length"],
// use that length to access the last index of T
> = T[L1];
// Example:
type Foo = TakeLast<[1, 5, 7, 8, 9, string]>; // string
2. The type of all items in the tuple BUT the last.
This one is tricky and isn't working 100% yet. The basic idea is to work with the reduced tuple again and compare indizes with the original tuple:
type MapTuples<T1 extends any[], T2 extends any[]> = { [K in keyof T1]: K extends keyof T2 ? T1[K] : never };
type Bar = MapTuples<[1, 2, 3], [4, 5]>; // [1, 2, never]
type Baz = MapTuples<[1, 2], [3, 4, 5]>; // [1, 2]
As you can see, when T1
is longer than T2
, the extra elements get converted to never
. In comparison we can use this trick to erase properties from objects, which does not work here. As a result, when we use the following types to try and drop the last argument from a parameter list
type DropLast<
T extends any[],
MinusOne extends any[] = Drop1<T>,
> = MapTuples<T, MinusOne>;
// DropLast<[1, 2, 3]> is [1, 2, never]
// Returns the params of a function as a tuple
type Params<F extends (...args: any[]) => void> = F extends ((...args: infer TFArgs) => any) ? TFArgs : never;
// creates a function type with one less argument than the given one
type DropLastArg<
F extends (...args: any[]) => void,
FArgs extends any[] = Params<F>,
RArgs extends any[] = DropLast<FArgs> // ** ERROR **
> = (...args: RArgs) => void;
the last argument is still present, but now has type never
type F1 = DropLastArg<(arg1: number, arg2: string, arg3: boolean) => void>;
// F1 is (arg1: number, arg2: string, arg3: never) => void
In addition, the mapped tuple type is no longer recognized as a tuple [Symbol.iterator()] is missing in type MapTuples<...>
, so we have to force RArgs to be one
type ForceTuple<T> = T extends any[] ? T : any[];
type DropLastArg<
F extends (...args: any[]) => void,
FArgs extends any[] = Params<F>,
RArgs extends any[] = ForceTuple<DropLast<FArgs>>
> = (...args: RArgs) => void;
but now F1
has type (...args: any[]) => void
because we lost the type information.
However with a few changes, we can get closer to the desired result:
// notice how we now Map from T1 to T2
type MapTuples<T1 extends any[], T2 extends any[]> = { [K in keyof T1]: K extends keyof T2 ? T2[K] : never };
// MapTuples<[1, 2], [4, 5, 6]> is [4, 5]
type DropLast<
T extends any[],
// create a tuple that is 1 shorter than T
MinusOne extends any[] = Drop1<T>,
// and map the entries to the ones at the corresponding indizes in T
> = MapTuples<MinusOne, T>;
// DropLast<[1, 2, 3]> is [1, 2] :)
type F1 = DropLastArg<(arg1: number, arg2: string, arg3: boolean) => void>;
// F1 is (arg2: number, arg3: string) => void
Notice how F1 has the correct argument types, but the names are off by one!
Suggestion
So in conclusion I'd like to see some more improvements to mapped tuples, specifically:
- the ability to remove items from them and
- complex mapped tuples to still be recognized as tuples.
Use Cases
A BIG usecase is typing NodeJS callback-style APIs, which I came very close to type using this:
type Drop1<T extends any[]> = ((...args: T) => void) extends ((a1: any, ...rest: infer R) => void) ? R : never;
type TakeLast<
T extends any[],
// Create a tuple which is 1 item shorter than T and determine its length
L1 extends number = Drop1<T>["length"],
// use that length to access the last index of T
> = T[L1];
type MapTuples<T1 extends any[], T2 extends any[]> = { [K in keyof T1]: K extends keyof T2 ? T2[K] : never };
type DropLast<
T extends any[],
// create a tuple that is 1 shorter than T
MinusOne extends any[] = Drop1<T>,
// and keep only the entries with a corresponding index in T
> = MapTuples<MinusOne, T>;
type Params<F extends (...args: any[]) => void> = F extends ((...args: infer TFArgs) => any) ? TFArgs : never;
type ForceTuple<T> = T extends any[] ? T : any[];
type ForceFunction<T> = T extends ((...args: any[]) => any) ? T : ((...args: any[]) => any);
type Promisify<
F extends (...args: any[]) => void,
// Extract the argument types
FArgs extends any[] = Params<F>,
// Infer the arguments for the promisified version
PromiseArgs extends any[] = ForceTuple<DropLast<FArgs>>,
// Parse the callback args
CallbackArgs extends any[] = Params<ForceFunction<TakeLast<FArgs>>>,
CallbackLength = LengthOf<CallbackArgs>,
TError = CallbackArgs[0],
// And extract the return value
TResult = 1 extends CallbackLength ? void : CallbackArgs[1]
> = (...args: PromiseArgs) => Promise<TResult>;
Examples
type F1 = (arg1: number, arg2: string, c: (err: Error, ret: boolean) => void) => void;
type F1Async = Promisify<F1>;
// F1Async is (arg2: number, c: string) => Promise<boolean>; (YAY!)
Checklist
My suggestion meets these guidelines:
- This wouldn't be a breaking change in existing TypeScript / JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. new expression-level syntax)