Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Further improve mapped type support for tuples and arrays (almost complete Promisify<T> inside!) #26190

Open
4 tasks done
AlCalzone opened this issue Aug 3, 2018 · 2 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@AlCalzone
Copy link
Contributor

AlCalzone commented Aug 3, 2018

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)
@sirian
Copy link
Contributor

sirian commented May 23, 2019

@AlCalzone
DropLast<[1 | undefined, 2?]> narrows to [(1 | undefined)?] instead of [1 | undefined]

@AlCalzone
Copy link
Contributor Author

@sirian When I wrote those types last august, they used to work. But since they involve a lot of hacky workarounds, it is very likely that they don't work anymore.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants