Home > front end >  Conditional types in Typescript with/without generics seems inconsistent
Conditional types in Typescript with/without generics seems inconsistent

Time:12-10

While reading the section on Conditional Types in the official Typescript Handbook, I came across this behavior in Typscript that I did not understand.

Paraphrasing the example given in the section, here's my example:

type ConditionalType<T> = T extends number ? "SUCCESS" : "FAIL";  // conditional type defined using generics

// source of confusion
type Result1 = (string | number) extends number ? "SUCCESS" : "FAIL";   // Result1 is "FAIL"
type Result2 = ConditionalType<string | number>;  // Result2 is "FAIL" | "SUCCESS"

// sanity check
type Result3 = ConditionalType<number>;  // Result3 is "SUCCESS"
type Result4 = ConditionalType<boolean>;  // Result4 is "FAIL"

Here's a recreation of the above example on the official Typescript playground: [LINK]

I don't understand why Result1 and Result2 are different. According to me, Result2 should also have been "FAIL".

CodePudding user response:

This is a consequence of a section a bit lower namely Distributive conditional types

Basically if typescript sees a condition over a naked type parameters (such as T) and if T is a union, it will apply the conditional type to each constituent of the union and union the results.

So we have:

type Result2 = ConditionalType<string | number> 
    ≡ ConditionalType<string> | ConditionalType<number>
    ≡ (string extends number ? "SUCCESS" : "FAIL") | (number extends number ? "SUCCESS" : "FAIL")
    ≡ "FAIL" | "SUCCESS"

This will not happen for Result1 where the condition is over a type not a type parameter, so inlining a conditional type can produce different results.

We can get the same behavior if we introduce a type parameter in the inlined version:

type Result1 = (string | number) extends infer T ? T extends number ? "SUCCESS" : "FAIL" : never;   // Result1 is "FAIL" | "SUCCESS"

Playground Link

In the example above, infer T will introduce a new type parameter that will contain the type on the left side of extends in this case string | number

We can also disable distribution for the conditional type if we wrap the type parameter in a tuple:

type ConditionalType<T> = [T] extends [number] ? "SUCCESS" : "FAIL";  // conditional type defined using generics
type Result2 = ConditionalType<string | number>;  // Result2 is "FAIL"

// sanity check
type Result3 = ConditionalType<number>;  // Result3 is "SUCCESS"
type Result4 = ConditionalType<boolean>;  // Result4 is "FAIL"

Playground Link

CodePudding user response:

I didn't know the conditional types, but now I learn something new!

You're passing an union of possible types to the ConditionalType, thus the actual constraint is not known.

It becomes more clear as in the handbook example:

function test<T extends number | string>(): ConditionalType<T> {
   throw new Error();
}

const z1 = test<string>();  //FAIL
const z2 = test<number>();  //SUCCESS

The same function gives two possible results upon the generic argument type.

For Result1, everything is known: no parameters, no variability, hence the result is also known.

  • Related