microsoft / microsoft/TypeScript

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

Aperta
#26,190 2 commenti 7 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

In Discussion Suggestion
Lingua principale
Go
Stelle
111k
Fork
14.4k
Merge medio
1g 19h
PR unite (30g)
117

Descrizione

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:

// 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)

Guida per i contributori

Apri la guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Direzione di ricerca

Inizia riproducendo gli esempi di tuple mappate e il caso d’uso di Promisify descritti nell’issue. Esamina il comportamento del type-checker di TypeScript per le tuple mappate, quindi determina se la rimozione degli elementi delle tuple e il riconoscimento delle tuple possono essere supportati senza perdere i tipi o i nomi degli elementi. Il lavoro è completo quando gli esempi mostrati producono i tipi di tupla e di funzione previsti, con gli opportuni test del compilatore.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
typescript
Ambito
compilers
Tipo di issue
Funzionalità
Difficoltà
5/5
Tempo stimato
Più di una settimana
Stato di attività
Ferma
Chiarezza
Abbastanza chiara
Idoneità per principianti
25/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.