Skip to content

Feature request: lift the circular constraint for conditional types #26980

Closed

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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

No one assigned

    Labels

    Fix AvailableA PR has been opened for this issueNeeds ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.SuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions