Description
Search Terms
circular type conditional type
Suggestion
Currently, Last1
is valid, but seemingly-equivalent Last2
is not:
type Head<T extends any[]> = T extends [infer X, ...any[]] ? X : never;
type Tail<T extends any[]> =
((...x: T) => void) extends ((x: any, ...xs: infer XS) => void) ? XS : never;
type Last1<T extends any[]> = {
0: never,
1: Head<T>,
2: Last1<Tail<T>>,
}[T extends [] ? 0 : T extends [any] ? 1 : 2];
type Last2<T extends any[]> =
T extends [] ? never :
T extends [infer R] ? R :
Last2<Tail<T>>;
My suggestion is to make Last2
valid, since the recursion isn't necessarily unbounded.
Use Cases
It'd make recursive conditional types a lot easier and more intuitive to write. It'd also eliminate the need to guard against impossible cases when using the workaround like when using Last1
above. If you wanted to mandate termination, all I'd want is direct recursion - this makes it much easier to check for infinite recursion. (Other types are generally assumed to terminate, so you're only assessing control flow.)
Examples
See above in the suggestion summary. Here's a concrete example of how this file (with dependency) would be simplified:
type Last<L extends any[], D = never> =
L extends [] ? D :
L extends [infer H] ? H :
((...l: L) => any) extends ((h: any, ...t: infer T) => any) ? Last<T> :
D;
type Append<T extends any[], H> =
((h: H, ...t: T) => any) extends ((...l: infer L) => any) ? L : never;
type Reverse<L extends any[], R extends any[] = []> =
((...l: L) => any) extends ((h: infer H, ...t: infer T) => any) ?
Reverse<T, Append<R, H>> :
R;
type Compose<L extends any[], V, R extends any[] = []> =
((...l: L) => any) extends ((a: infer H, ...t: infer T) => any) ?
Compose<T, H, Append<R, (x: V) => H>> :
R;
export type PipeFunc<T extends any[], V> =
(...f: Reverse<Compose<T, V>>) => ((x: V) => Last<T, V>);
Currently, you could achieve similar via this, but as you can see, it's highly repetitive and boilerplatey:
type Last<L extends any[], D = never> = {
0: D,
1: L extends [infer H] ? H : never,
2: ((...l: L) => any) extends ((h: any, ...t: infer T) => any) ? Last<T> : D,
}[L extends [] ? 0 : L extends [any] ? 1 : 2];
type Append<T extends any[], H> =
((h: H, ...t: T) => any) extends ((...l: infer L) => any) ? L : never;
type Reverse<L extends any[], R extends any[] = []> = {
0: R,
1: ((...l: L) => any) extends ((h: infer H, ...t: infer T) => any) ?
Reverse<T, Append<R, H>> :
never,
}[L extends [any, ...any[]] ? 1 : 0];
type Compose<L extends any[], V, R extends any[] = []> = {
0: R,
1: ((...l: L) => any) extends ((a: infer H, ...t: infer T) => any) ?
Compose<T, H, Append<R, (x: V) => H>>
: never,
}[L extends [any, ...any[]] ? 1 : 0];
export type PipeFunc<T extends any[], V> =
(...f: Reverse<Compose<T, V>>) => ((x: V) => Last<T, V>);
(The original code snippet is linked to from here as a possible solution to the issue of _.compose
, _.flow
, and friends.)
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)