Repository: TypeScript
Issue: Further improve mapped type support for tuples and arrays (almost complete Promisify<T> inside!)
Labels: Suggestion,In Discussion

Now that #26063 is merged (which is great BTW), I was toying around with an alternate solution to https://github.com/Microsoft/TypeScript/issues/1360 or https://github.com/Microsoft/TypeScript/issues/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:**  
```ts
// 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:
```ts
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
```ts
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`
```ts
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
```ts
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:
```ts
// 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:
```ts
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

```ts
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:
* [x] This wouldn't be a breaking change in existing TypeScript / JavaScript code
* [x] This wouldn't change the runtime behavior of existing JavaScript code
* [x] This could be implemented without emitting different JS based on the types of the expressions
* [x] This isn't a runtime feature (e.g. new expression-level syntax)