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"
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"
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.