Introduction to Conditional Types: Types Based on Conditions
Introduction
Conditional types are one of TypeScript's most powerful type-system features. They let you express logic at the type level: "if A extends B then X else Y." For intermediate developers this opens up advanced patterns for transforming types, deriving new types from existing ones, and building safer APIs without runtime overhead.
In this tutorial you will learn what conditional types are, why they matter, and how to use them effectively. We'll cover basic syntax, distributive behavior, the infer keyword, common patterns like filtering unions and extracting element types, and how to combine conditional types with mapped and utility types to build real-world helpers.
Expect lots of hands-on examples and step-by-step explanations. By the end you'll be able to implement type-level guards, create custom utility types, and reason about complex type expressions more confidently. We'll also tie conditional types into other core TypeScript topics — generics, narrowing, and standard utility types — so you have a practical toolbox for day-to-day use.
This guide targets intermediate TypeScript developers who already understand interfaces, union types, and generics. If you know how to read or write basic generic functions and properties, you're ready. We'll link to related deeper guides so you can dive into supporting topics like utility types, NonNullable, and type narrowing.
Background & Context
Conditional types were introduced to provide compile-time branching for types. Syntax is simple: T extends U ? X : Y. Despite the simple form, conditional types unlock sophisticated patterns because types themselves can be unions, mapped types, or results of other conditional types.
Why use conditional types?
- They let you transform types based on conditions (e.g., unwrap promises or arrays).
- They enable type-level filtering and mapping without runtime cost.
- They integrate with other type features such as infer, distributive behavior, and mapped types to express complex invariants.
If you want a broader sense of how utility types transform types, review our introduction to utility types for more context on how conditional types underpin common helpers: Introduction to Utility Types: Transforming Existing Types.
Key Takeaways
- Understand the syntax T extends U ? X : Y and when it applies.
- Learn how distributive conditional types work with unions.
- Use infer to capture subtypes inside conditional branches.
- Build common utilities: UnwrapPromise, FilterUnion, ElementType, and custom Nullable handling.
- Combine conditional types with mapped types and standard utilities for real-world APIs.
- Avoid common pitfalls like accidental distributivity and overly complex nested types.
Prerequisites & Setup
You should have TypeScript installed (v3.5+ recommended; many features stabilize in later versions). A typical setup:
- Node.js and npm
- TypeScript: npm install -D typescript
- A code editor with TypeScript support (VS Code is recommended)
Familiarity required:
- Generics and generic constraints (see Introduction to Generics: Writing Reusable Code).
- Basic utility types like Partial, Pick, and Readonly (see Using Partial
: Making All Properties Optional and Using Readonly: Making All Properties Immutable ).
Open a project and create a file types.ts to follow along. Use tsc --noEmit to run type-checks.
Main Tutorial Sections
1) Basic Conditional Types — Syntax and Examples
Conditional types follow the pattern:
type Result<T> = T extends string ? "a string" : "not a string"; type A = Result<string>; // "a string" type B = Result<number>; // "not a string"
Explanation: If T can be assigned to string then the type resolves to the "true" branch; otherwise to the "false" branch. This is evaluated at compile time and allows you to compute different types based on input generics. Use this to build type-level switches.
2) Distributive Conditional Types — When Unions Matter
Conditional types distribute over naked unions on the left-hand side of extends. Example:
type Wrap<T> = T extends any ? { value: T } : never;
type X = Wrap<string | number>; // { value: string } | { value: number }Because the type parameter is a union, the conditional is evaluated for each union member independently. This is very useful to apply transformations to each member of a union. But be careful: if you want to avoid distribution, wrap with a tuple:
type NoDist<T> = [T] extends [string | number] ? true : false;
This stops the distribution and evaluates the union as a whole.
3) Using infer to Capture Types
The infer keyword lets you bind a type variable inside a conditional branch. It's commonly used to extract inner types:
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T; type T1 = UnwrapPromise<Promise<string>>; // string type T2 = UnwrapPromise<number>; // number
You can use infer with tuples, functions, and other structures:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
This is how TypeScript's built-in ReturnType is implemented — a robust way to lift information from complex types.
4) Filtering Unions with Extract and Exclude
Conditional types are the mechanism behind utilities that let you filter unions. For example, Extract<T, U> keeps members assignable to U; Exclude<T, U> removes them. These are implemented with conditional types under the hood:
type MyExtract<T, U> = T extends U ? T : never; type MyExclude<T, U> = T extends U ? never : T;
For a deep-dive and practical examples, see our guide on Deep Dive: Using Extract<T, U> to Extract Types from Unions and Using Exclude<T, U>: Excluding Types from a Union.
Practical tip: use Extract when you want one branch of a union and Exclude to carve away unwanted parts.
5) Conditional Types Combined with Mapped Types
Conditional types can be used inside mapped types to transform property types based on conditions:
type NullableKeys<T> = {
[K in keyof T]: undefined extends T[K] ? K : never
}[keyof T];
type NullableProps<T> = Pick<T, NullableKeys<T>>;This pattern extracts keys whose values allow undefined and then picks those properties. You can reason about property-level decisions and build utilities like RemoveNullable or Optionalize. See also Using Pick<T, K>: Selecting a Subset of Properties and Using Omit<T, K>: Excluding Properties from a Type for related transformations.
6) Conditional Types over Tuples and Arrays
Use conditional types with infer to extract element types from arrays and tuples:
type ElementType<T> = T extends (infer U)[] ? U : T extends readonly (infer V)[] ? V : never; type E1 = ElementType<string[]>; // string type E2 = ElementType<readonly number[]>; // number
Also you can pattern-match tuples to split head and tail:
type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never;
This makes tuple-level operations (like type-safe array manipulation) possible.
7) Building Utility Types: NonNullable & More
Conditional types power useful helpers like NonNullable, which removes null and undefined. Here's a simplified version:
type MyNonNullable<T> = T extends null | undefined ? never : T;
This is similar to the built-in Using NonNullable
8) Conditional Types with Generics & Constraints
Conditional types work well with generic constraints. For example, create a helper that returns object keys whose values are functions:
type FunctionKeys<T> = {
[K in keyof T]: T[K] extends Function ? K : never
}[keyof T];
type API = { login: () => void; name: string };
type FKeys = FunctionKeys<API>; // "login"If you need to enforce constraints at the generic level, read more on Constraints in Generics: Limiting Type Possibilities and general generic patterns in Generic Functions: Typing Functions with Type Variables.
9) Combining Conditional Types with Standard Utilities
You can mix conditional types with built-in utilities like Partial, Pick, Omit, and Readonly. For example, make fields optional only if they are nullable:
type OptionalIfNullable<T> = {
[K in keyof T as undefined extends T[K] ? K : never]?: T[K]
} & {
[K in keyof T as undefined extends T[K] ? never : K]: T[K]
};This hybrid approach is powerful when creating API payload types. Explore related helpers: Using Partial
10) Real Patterns: Tagged Unions and Narrowing
Conditional types can infer the shape of discriminated unions for safe accessors. Example: extract union member by tag:
type ByTag<T, Tag extends string> = T extends { type: Tag } ? T : never;
type Event = { type: 'click'; x: number } | { type: 'keydown'; key: string };
type ClickEvent = ByTag<Event, 'click'>; // { type: 'click'; x: number }Use this to build safe handlers, and combine with runtime checks and narrowing techniques like Type Narrowing with instanceof Checks in TypeScript and Type Narrowing with typeof Checks in TypeScript for complete patterns that span types and runtime.
Advanced Techniques
Once comfortable, explore advanced patterns:
- Nested infer and conditional composition: extract deeply nested structures (e.g., unwrap Promise<Promise
> recursively). - Recursive conditional types to normalize nested container types; be mindful of compiler recursion limits.
- Conditional mapped types with key remapping (as introduced above) to build precise shape transformations.
- Type-level computation using distributive behavior to map different handlers across union members.
For API-heavy codebases, combine conditional types with generic classes or interfaces to express compile-time invariants. See concepts in Generic Interfaces: Creating Flexible Type Definitions and Generic Classes: Building Classes with Type Variables for integrating these patterns into object-oriented designs.
Performance tip: Type complexity can slow down editor feedback. Try to keep types shallow where possible, and prefer smaller composable types instead of very deep one-off expressions.
Best Practices & Common Pitfalls
Dos:
- Keep conditional types focused and documented — complex type logic benefits from comments and small helper types.
- Use tuple wrapping to prevent undesired distributivity when you need to treat a union as a single entity: [T] extends [U].
- Rely on standard utilities when possible (Pick/Omit/Extract/Exclude/NonNullable) — they are well-tested and readable.
Don'ts:
- Don't create massive nested conditional expressions if a simpler runtime check with validation would be more maintainable.
- Avoid overuse of infer in places where explicit generics or type aliases improve clarity.
- Don't silence errors with type assertions casually — review the risks in Type Assertions (as keyword or <>) and Their Risks.
Troubleshooting:
- If you see "Type instantiation is excessively deep and possibly infinite" split the type into smaller aliases and validate step-by-step.
- Use quick checks with small example types in a scratch file to reason about intermediate type results.
Real-World Applications
Here are practical places where conditional types shine:
- API clients: derive request/response types, optional/required fields, and filtered payloads.
- ORM type maps: infer model attribute types and relations from schema definitions.
- UI component libraries: derive prop types based on generic parameters (e.g., controlled vs uncontrolled components).
- Library authors: expose ergonomically typed helpers that compute return types from input parameters.
For example, if you build an SDK that returns different shape payloads for different resources, conditional types let you express accessors that map resource names to exact response shapes without duplicating types.
Conclusion & Next Steps
Conditional types are a cornerstone of advanced TypeScript. They let you compute types from conditions, enabling safer, more expressive APIs without runtime costs. Next steps:
- Practice by implementing small helpers: UnwrapPromise, ElementType, and FilterByTag.
- Study utility type implementations (like Extract, Exclude, NonNullable) for patterns to reuse.
- Explore deeper topics like generic constraints and advanced narrowing to combine type- and runtime-safety. See our walkthrough on Introduction to Generics: Writing Reusable Code.
Enhanced FAQ
Q1: What exactly is distributive conditional type behavior? A1: When a conditional type's checked type parameter is a naked union, TypeScript distributes the conditional across each union member. For instance type T extends U ? X : Y with T = A | B becomes (A extends U ? X : Y) | (B extends U ? X : Y). This allows per-member transformations but can be undesirable when you want to treat the union as a whole — wrap the type in a tuple ([T] extends [U]) to prevent distribution.
Q2: When should I use infer inside conditional types? A2: Use infer when you want to capture a portion of a type inside the conditional branch. Common use-cases: extracting return types, element types, or inner generic arguments from containers (Promise, Array, etc.). Keep infer variable names descriptive, and try to expose the resulting type via reusable aliases for clarity.
Q3: How do conditional types relate to mapped types and utility types? A3: Conditional types can appear inside mapped types to compute property-level transformations. Many utility types like Extract, Exclude, and NonNullable are implemented with conditional types. Conditional types are a complement to mapped types — use mapped types for structure (property iteration) and conditional types for branch logic.
Q4: Can conditional types recurse? A4: Yes, conditional types can be recursive, but TypeScript enforces recursion limits and may report errors like "Type instantiation is excessively deep." When recursion is needed, split logic into smaller parts and ensure termination conditions are explicit.
Q5: How do I debug complex conditional types? A5: Debug by isolating parts into named type aliases and check each alias in a small test file. Use concrete types (not generics) to inspect the resolved type in your editor's hover tool. This incremental approach helps identify where a condition resolves differently than expected.
Q6: Are conditional types runtime code? A6: No — conditional types exist only at compile time. They don't generate runtime JS. This means they are free in terms of runtime cost but also cannot enforce behavior at runtime — you still need validators for user input or external data.
Q7: How do conditional types interact with narrowing and runtime checks? A7: Conditional types express type-level decisions. At runtime you still need narrowing (typeof, instanceof, tag checking) to guarantee that a value meets the type. See guides on Type Narrowing with typeof Checks in TypeScript and Type Narrowing with instanceof Checks in TypeScript for patterns to bridge runtime checks and compile-time types.
Q8: When should I prefer runtime validation over complex type logic? A8: Use compile-time types for internal API consistency and developer ergonomics. Use runtime validation (e.g., zod, io-ts) when dealing with external input (HTTP, user input) because types cannot guarantee runtime shape. Complex conditional types are best for internal abstractions where the compiler and developers control the data flow.
Q9: How do Extract and Exclude differ from filtered mapped types? A9: Extract<T, U> is a conditional-type-based helper that keeps union members assignable to U. Exclude<T, U> removes them. Filtered mapped types operate at the property level and transform or drop properties from object types. Both are valuable — choose union-level utilities for unions and mapped transformations for object shapes. See Deep Dive: Using Extract<T, U> to Extract Types from Unions for more.
Q10: Any performance considerations for large codebases? A10: Editor responsiveness can degrade with extremely complex types. Keep types modular, reduce unnecessary nesting, and prefer explicit types in hot paths. If you encounter slowdowns, split types into smaller named aliases so the compiler caches and computes them incrementally.
This tutorial should give you a strong practical foundation for conditional types. Keep practicing by implementing and reading real-world utility types, and revisit related topics like generics and narrowing as you design more advanced type systems. For practical guidance on related transforms, see Using Pick<T, K>: Selecting a Subset of Properties and Using Omit<T, K>: Excluding Properties from a Type.
Happy typing!
