typescript: Using infer with never get different result

Viewed 184

I made a few demos to reproduce my problems. Here they are.

Question1: why does type g get 2?

// 1
type f = [never] extends [infer S1, ...infer S2]
  ? ([S1] extends [never] ? 1 : 2)
  : 3
// why 2
type g = [never] extends [infer S1, ...infer S2]
  ? ([never] extends [S1] ? 1 : 2)
  : 3

From my perspective, type f equals g because S1 is never.

Question2: why does type h get 2?

// 1
type i = [never] extends [infer S1]
  ? ([never] extends [S1] ? 1 : 2)
  : 3
// why 2
type h = [never] extends [infer S1, ...infer S2]
  ? ([never] extends [S1] ? 1 : 2)
  : 3

From my perspective, type i equals h because S2 is unused.

Question3: why does type k get never?

// 1
type j = [never] extends [infer S1, ...infer S2]
  ? (never extends S1 ? 1 : 2)
  : 3
// why never ?
type k = [never] extends [infer S1, ...infer S2]
  ? (S1 extends never ? 1 : 2)
  : 3

I couldn't understand even a little. I thought k should be one of 1 2 3 at least.

1 Answers

Your questions 1 and 2 seem to be exposing a bug in TypeScript.

I also expect your F, G, H, and I types (non primitive types are conventionally written in UpperCamelCase and not lowerCamelCase, which makes them look like variable names) to evaluate to 1. The fact that G and H evaluate to 2 is surprising. My research indicates that this behavior is new; you can verify that your code works as expected in TypeScript 4.1.5, but fails in TypeScript 4.2.2. Specifically, this change happened on or around January 12 or 13 of 2021. Your code works as expected in TypeScript 4.2.0-dev.20210112, but fails in TypeScript 4.2.0-dev.20210113.

It seems very likely that this behavior was introduced when microsoft/TypeScript#42248 was merged into the main code branch. This pull request fixed a bug (microsoft/TypeScript#39992) but seems to have caused a few new bugs, such as microsoft/TypeScript#42331.

I have filed microsoft/TypeScrpit#43213 to track this issue. For now I'd say that if you have a real use case that needs to be constructed like your G and H above, and if you cannot wait for this issue to be fixed, then you work around it by using a type alias instead of the bare never, like this:

type N = never
type Gfixed = [N] extends [infer S1, ...infer S2] ? ([N] extends [S1] ? 1 : 2) : 3 // 1

Your question 3 is behaving as expected. In type K, (S1 extends never ? 1 : 2) is a distributive conditional type because the type being checked, S1, is a type parameter. And such a type function distributes over unions; meaning that if F<T> is distributive, then F<X | Y> will be equivalent to F<X> | F<Y>.

This implies that F<never> should be never (or at least that it would be a subtype of all possible answers for F<T>). Why? Because X | never is equivalent to X, so F<X | never> must be equivalent to F<X> | F<never>. This means that for all X, F<X> | F<never> is equivalent to F<X>. Conceptually, then, F<never> should be never.

And it turns out that the compiler does in fact treats never as "the empty union" when used in a distributive conditional type. This behavior definitely surprised me when I first ran into it, because it takes a certain shift in mindset to think of never as any kind of union.

Observe:

type Distributive<T> = T extends string ? { a: T } : { b: T };

type X = Distributive<"a"> // {a: "a"}
type Y = Distributive<0> // {b: 0}
type Z = Distributive<"a" | 0> // {a: "a"} | {b: 0}

type StillX = Distributive<"a" | never> // {a: "a"}
type Never = Distributive<never> // never

Playground link to code

Related