Mastering Conditional Types in TypeScript (T extends U ? X : Y)
Introduction
Conditional types are one of TypeScript's most powerful and expressive features. They let you compute a type based on another type, using a syntax that looks like a ternary expression: T extends U ? X : Y. For intermediate developers, conditional types unlock advanced API design, safer abstractions, and the ability to encode logic at the type level rather than runtime.
In this tutorial you will learn what conditional types are, how they work under the hood, and when to use them. We will cover simple conditional mappings, distributive conditional types, inference inside conditional types, working with unions, combining conditional types with mapped and utility types, and common pitfalls to avoid. Each section includes practical code examples, step-by-step reasoning, and troubleshooting tips so you can apply conditional types in real-world code.
By the end of this article you will be comfortable reading and authoring conditional types that transform and validate shapes, produce safer libraries, and provide rich developer ergonomics via precise typing. You will also see how conditional types fit with generics, constraints, and other type system tools and where to avoid overcomplicating your types.
Background & Context
Conditional types follow the pattern:
type Result<T> = T extends U ? X : Y
At its core, the type system asks: does T extend U? If yes, it resolves to X; otherwise to Y. However, that simple description hides several important behaviors: conditional types are distributive over naked type parameters in unions, they can use infer to capture parts of a type, and they interact with utility types such as Exclude, Extract, and NonNullable to provide expressive transformations.
Understanding conditional types is critical when building libraries or complex type-level operations. They let you compute return types for generic functions, narrow union members, extract payloads from containers, and express compile-time validation rules. Conditional types are also often used together with generics and constraints to build ergonomic APIs — see our guide on Introduction to Generics for a broader grounding.
Key Takeaways
- Conditional types use the syntax T extends U ? X : Y to compute types.
- When T is a union and the type parameter is naked, the conditional is distributive.
- Use infer inside conditional types to capture and reuse parts of a type.
- Conditional types combine powerfully with utility types like Deep Dive: Using Extract<T, U> to Extract Types from Unions and Using Exclude<T, U>: Excluding Types from a Union.
- Prefer readability: complex nested conditional types have maintenance costs.
Prerequisites & Setup
To follow along you should have a working TypeScript environment. Install TypeScript globally or in your project:
npm install --save-dev typescript
Use an editor with TypeScript support, like VS Code, to get immediate feedback. Familiarity with generics, union and intersection types, and the basic utility types will help. If you want a refresher on generics or generic functions refer to Generic Functions and Constraints in Generics.
Main Tutorial Sections
1) Basic conditional type: a first example (100-150 words)
A minimal conditional type returns one of two types depending on whether a type extends another. Consider:
type IsString<T> = T extends string ? true : false type A = IsString<string> // true type B = IsString<number> // false
This is straightforward for concrete types. When T is a type parameter and receives a union, the behavior changes (see distributive section). Use this pattern to express compile-time feature flags or simple shape checks. For example, you can choose between number and string encodings based on a literal type parameter.
2) Distributive conditional types explained (100-150 words)
A key nuance: if the checked type is a naked type parameter, conditional types distribute over unions. Example:
type ToArray<T> = T extends any ? T[] : never type R = ToArray<string | number> // string[] | number[]
This happens because the conditional type is applied to each union member. You can prevent distribution by wrapping the parameter in a tuple:
type NotDistributive<T> = [T] extends [U] ? X : Y
Distributivity is useful for transforming unions memberwise, but it can surprise you when you expect a single composite result.
3) Using infer to extract parts of types (100-150 words)
The infer keyword captures parts of a type inside a conditional type. Example: extracting the item type from a Promise or array:
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T type P = UnwrapPromise<Promise<string>> // string
You can capture multiple infered parts and use constraints to further validate shapes. Combined with union distributions, infer becomes a powerful tool to pattern match types. Common use cases include extracting payloads from wrapper types, deriving argument or return types, and building generic utilities.
4) Conditional types and unions: combining with Extract and Exclude (100-150 words)
Conditional types are the conceptual basis for utility types like Extract and Exclude. For instance, Extract<T, U> returns T extends U ? T : never. See our deep dive on Deep Dive: Using Extract<T, U> to Extract Types from Unions and Using Exclude<T, U>: Excluding Types from a Union for details.
type MyExtract<T, U> = T extends U ? T : never
Use these patterns to pick members of a union that match a constraint or to strip out incompatible members. They are especially handy when paired with distributivity.
5) Conditional types and type narrowing interplay (100-150 words)
Conditional types model type relationships statically, while narrowing via typeof, instanceof, and the in operator happen at runtime. Still, conditional types can encode the same logic at compile time. If you combine runtime narrowing with static conditional types you get excellent ergonomics: typed guards narrow values at runtime and type-level utilities shape the types.
For runtime narrowing patterns, check our guides on Type Narrowing with typeof Checks in TypeScript, Type Narrowing with instanceof Checks in TypeScript, and Type Narrowing with the in Operator in TypeScript.
6) Conditional types with mapped types and utility types (100-150 words)
Conditional types pair well with mapped types to transform object shapes. For example, create a type that maps optional properties to non-optional based on a condition:
type MakeRequiredIf<T, K extends keyof T> = {
[P in keyof T]: P extends K ? NonNullable<T[P]> : T[P]
}You can also reuse existing utility types to simplify logic, such as Using NonNullable
7) Conditional types in function return types (100-150 words)
Conditional types shine when computing a function return type based on input types. Consider a serializer that returns different types based on an options flag:
type Serialized<T, AsString extends boolean> = AsString extends true ? string : T
function serialize<T, AsString extends boolean = false>(value: T, asString?: AsString): Serialized<T, AsString> {
return (asString ? JSON.stringify(value) : value) as any
}
const s1 = serialize({a: 1}, true) // string
const s2 = serialize({a: 1}, false) // {a: number}This pattern helps you model API ergonomics at compile time and avoid runtime type assertions.
8) Preventing unwanted distribution (100-150 words)
Sometimes you want the conditional not to distribute across a union. A common trick is to wrap types in a tuple or use an extra generic parameter:
type NoDist<T> = [T] extends [string | number] ? 'ok' : 'not' type A = NoDist<string | boolean> // 'not'
Another approach uses helper types that control whether a type is considered naked. Understanding distribution is essential to prevent subtle bugs when composing conditional logic.
9) Composing conditional types for complex logic (100-150 words)
You can chain conditional types to express multi-branch logic, but readability matters:
type MapType<T> = T extends string ? 's' : T extends number ? 'n' : 'o'
For more maintainable code, split logic into named intermediate types and document intentions. When code grows, consider using comments and tests (type-only tests using tsd or dts-jest) to ensure behavior remains correct across refactors.
Advanced Techniques (200 words)
Advanced conditional type techniques include leveraging infer with tuple and function types, making conditional types distributive on purpose, and combining conditional types with mapped types for deep transformations. For example, you can write a deep readonly mapper using recursive conditional types and mapped types to walk nested objects. Combining with utility types like Using Readonly
Using infer inside function signatures is powerful for extracting argument and return types:
type Args<T> = T extends (...a: infer A) => any ? A : never type R = Args<(a: string, b: number) => void> // [string, number]
Also consider performance and compiler complexity. Extremely complex conditional types can slow down the TypeScript server and increase editor latency. Use type-level tests and incremental refactors. When building libraries, prefer simpler exported types and hide complex internals behind private helper types and utility modules.
If you need to limit type parameters or ensure a generic meets expectations, combine conditional types with constraints. See Constraints in Generics for patterns that keep your generics predictable.
Best Practices & Common Pitfalls (200 words)
Dos:
- Prefer clarity over cleverness: name intermediate conditional types and split logic.
- Use comments and type tests to document intent and behavior.
- Leverage existing utility types like Using Exclude<T, U>: Excluding Types from a Union and Deep Dive: Using Extract<T, U> to Extract Types from Unions rather than reimplementing them ad hoc.
- Combine conditional types with runtime guards where appropriate; see Type Narrowing with typeof Checks in TypeScript for runtime patterns.
Donts and pitfalls:
- Avoid deep chains of nested conditional types with many infer nodes; they are hard to read and slow the compiler.
- Watch out for unwanted distribution. If behavior surprises you, test with both union and single-member cases.
- Beware of type explosion in complex mapped transforms. Using helper types and explicit constraints from Constraints in Generics can control shape.
Troubleshooting tips:
- Use temporary types and hover in your editor to see intermediate results.
- Add type assertions at boundaries to narrow the scope of complex generics.
- If editor performance degrades, simplify types or split a monolithic type into smaller exports.
Real-World Applications (150 words)
Conditional types are used in many real-world patterns:
- Library authoring: compute return types for flexible APIs and fluent builders.
- Serialization and parsing: map option flags to different output types.
- Data modeling: extract payload types from wrappers like Promise, Result, or API responses using infer.
- Form handling: mark fields required or optional conditionally and map nullable values using Using NonNullable
: Excluding null and undefined .
Example: building a typed event system where the event name maps to a payload type, a conditional type can select the payload based on the event key. Another example is writing validators whose return types reflect whether errors can be thrown or returned, enabling finer type-level guarantees for callers.
Conclusion & Next Steps (100 words)
Conditional types are a foundational technique for advanced TypeScript programming. They let you express logic at the type level, enabling safer APIs and more precise developer ergonomics. Start by practicing simple conditional types and progressively add infer and distributivity into your toolkit. Combine conditional types with generics, constraints, and utility types to build robust abstractions. Next, review practical generics patterns in Generic Functions and test your types with small, focused type tests.
Enhanced FAQ Section (300+ words)
Q1: What happens when T is a union in a conditional type? A1: If T is a naked type parameter and is a union, the conditional type is applied to each union member and the results are unioned. This is called distributivity. Wrap T in a tuple like [T] to prevent this behavior when you need the conditional evaluated for the entire union as a single type.
Q2: How does infer differ from using a generic parameter? A2: infer captures a type from within the matched pattern of a conditional type without adding a new generic parameter to the outer type. It is local to the conditional type and only available in the true branch, enabling pattern matching inside complex types like tuples, functions, and promises.
Q3: Can conditional types replace runtime type checks? A3: No. Conditional types are purely compile-time constructs. They help the compiler check and infer types but do not perform runtime validation. Combine conditional types with runtime guards for end-to-end safety. See runtime narrowing techniques in Type Narrowing with instanceof Checks in TypeScript and Type Narrowing with the in Operator in TypeScript.
Q4: Are conditional types slow for the compiler? A4: Complex conditional types can increase compile time and cause the TypeScript language server to lag in the editor. If you notice performance issues, simplify types, split large types into smaller named helpers, or hide complexity behind module boundaries.
Q5: How do I debug a complex conditional type? A5: Break the type into named intermediate types and hover them in your editor to see how TypeScript resolves each step. Use representative examples and temporary aliases to inspect behavior with unions and edge cases.
Q6: How do conditional types relate to utility types like Exclude and Extract? A6: Many utility types are built from conditional types. For example, Extract<T, U> is equivalent to T extends U ? T : never. Familiarity with conditional types helps you understand and build custom utilities that behave like these built-ins.
Q7: When should I stop using conditional types and use runtime checks instead? A7: Use conditional types for compile-time guarantees and ergonomics. If behavior depends on runtime data or environment, prefer runtime checks. For mixed scenarios, keep type-level logic minimal and use runtime guards for correctness.
Q8: Can conditional types be recursive? A8: Yes, you can write recursive conditional types to traverse nested data structures. Be careful: deep recursion can hit TypeScript's recursion limits or degrade performance. Use recursion judiciously and test behavior across expected inputs.
Q9: How do conditional types interact with mapped types?
A9: They complement each other. You can use conditional types inside mapped types to transform properties selectively, or wrap conditional logic around mapped transformations. For patterns that change property modifiers like readonly or optional, combining both yields expressive, maintainable results. See Using Readonly
Q10: Any tips for writing maintainable conditional types? A10: Name intermediate types, keep branches small, add comments describing intent, and add type-level tests. Prefer small, composable utilities over a single large type. When publishing libraries, consider hiding complex internals and exporting a small, well-documented surface.
Further reading and related guides that complement conditional type knowledge include Introduction to Utility Types: Transforming Existing Types, Type Assertions (as keyword or <> ) and Their Risks, and practical examples in Using Partial
If you want help turning a specific runtime pattern into a set of conditional types, share an example and I can walk through a step-by-step transformation and type-level test cases.
