Conditional Types in TypeScript allow you to describe one type based on another using the principle T extends U ? X : Y. Such types provide powerful capabilities when building complex type logic, especially in libraries and API declarations.
Example of a basic conditional type:
type IsString<T> = T extends string ? 'yes' : 'no'; type A = IsString<string>; // 'yes' type B = IsString<number>; // 'no'
When a conditional type is applied to a union type, TypeScript "distributes" the condition over each element of the union separately. This is called the distributive behavior of conditional types.
Example:
type Foo<T> = T extends { id: number } ? string : boolean; type Result = Foo<{id: number} | {name: string}>; // string | boolean
This is a very powerful feature, but it can lead to confusion regarding the expected result, especially when working with arrays and type mapping.
Wrap the type in a tuple:
type NoDistrib<T> = [T] extends [{id: number}] ? string : boolean; type Result = NoDistrib<{id: number} | {name: string}>; // boolean
Question: "What happens if you use a conditional type with union types without wrapping them in tuples? Is the result always the same as under normal logic?"
Answer: The result can be unexpected! Due to distributivity, the condition is applied to each member of the union types separately. To strictly compare the entire union type, you need to use a wrapper (tuple).
Example:
type Test<T> = T extends string ? number : boolean; type A = Test<string | boolean>; // number | boolean, not just boolean
Story
In a serialization library, a conditional type was used to check the structure of data, but they forgot to wrap the generic parameter in a tuple. As a result, on complex union types, the declarations broke, and the compiler produced unpredictable types when using the API.
Story
When trying to implement type transformation for processing model fields, part of the information was lost: due to distributivity creating unions, a couple of branches of logic were forgotten, and ultimately the typing became too permissive.
Story
A developer assumed that T extends SomeType inside conditional types would behave as expected for the entire object, but "spraying" happened — the compiler pointed out inconsistencies, leading to type chaos and serious bugs in auto-documentation generation.