Introduction
Conditional types represent one of TypeScript's most powerful features for type-level programming. They enable developers to express type-level logic that behaves like if-then-else statements, allowing types to adapt based on the characteristics of other types. This capability is especially valuable when working with generic types, as it allows you to create flexible type transformations that respond to the specific types they're given.
Unlike static types that remain fixed, conditional types evaluate at compile time to produce different type results depending on the input. This dynamic type resolution enables sophisticated type checking and transformation patterns that would be impossible with traditional type definitions alone. Whether you're filtering union types, extracting element types from arrays, or creating type-safe API response handlers, conditional types provide the foundation for robust, reusable type abstractions.
The syntax of conditional types mirrors JavaScript's ternary operator, making them familiar to developers transitioning from runtime to type-level programming. By mastering conditional types, you unlock the full potential of TypeScript's type system and gain the ability to create more precise, self-documenting code that catches errors at compile time rather than runtime.
Understanding Conditional Type Syntax
Basic Syntax and Structure
A conditional type follows a straightforward pattern that resembles a ternary operator: you specify a type relationship to check, and based on whether that relationship holds true, the conditional type resolves to one branch or another. The core syntax uses the extends keyword to establish the condition, followed by question mark and colon separators that divide the true and false branches.
// Basic conditional type syntax
type IsNumber<T> = T extends number ? true : false;
The fundamental structure reads as a question: if the type on the left is assignable to the type on the right, use the true branch; otherwise, use the false branch. This assignability check forms the basis of all conditional type logic.
When working with conditional types, proper formatting becomes essential for readability, especially with complex nested conditions. Breaking the conditional across multiple lines with consistent indentation helps clarify the structure and makes it easier to understand how nested conditions relate to each other.
1type IsNumber<T> = T extends number ? true : false;2 3// Usage examples4type Test1 = IsNumber<123>; // true5type Test2 = IsNumber<string>; // falseChaining and Nesting Conditional Types
Building Multi-Branch Type Logic
Just as JavaScript's ternary operator supports chaining, TypeScript's conditional types allow you to chain multiple conditions together to create sophisticated type-level decision trees. This chaining pattern proves particularly useful when you need to distinguish between many different types.
A practical example of chaining appears when building a type that determines the primitive type name for any given type. You can chain multiple conditional types together to test for undefined, null, boolean, number, bigint, and string in sequence. The type system evaluates each condition in order, returning the first matching type name or never if none of the conditions match.
1// Chained conditional type for primitive type names2type PrimitiveTypeName<T> =3 T extends undefined ? 'undefined' :4 T extends null ? 'null' :5 T extends boolean ? 'boolean' :6 T extends number ? 'number' :7 T extends bigint ? 'bigint' :8 T extends string ? 'string' :9 'object';Distributive Conditional Types
How Distribution Works
One of the most important behaviors of conditional types is their distributive nature over union types. When you apply a conditional type to a union type, TypeScript automatically distributes the conditional type over each member of the union, evaluating the condition separately for each type, and then combining the results into a new union. This behavior enables powerful patterns for transforming and filtering union types through type-level operations.
The distribution mechanism activates when the left-hand side of the extends clause is a bare type parameter. When you use a generic type parameter in this way, TypeScript automatically breaks down union types into their constituent members, evaluates the conditional type for each member, and collects the results. This automatic distribution happens transparently and forms the basis for many common conditional type patterns used in advanced web development.
1// Distributive conditional type2type WrapLen<T> = T extends { length: number } ? [T] : T;3 4// Distribution in action5type Result = WrapLen<boolean | 'hello' | Array<number>>;6// Result: boolean | ['hello'] | [Array<number>]Controlling Distributivity
Sometimes you want to prevent distribution and evaluate the conditional type against the union as a whole. TypeScript provides a simple mechanism: wrap the type parameter in a tuple type. By placing the type parameter inside square brackets, you create [T] instead of just T. This tuple wrapping prevents distribution because the left-hand side of extends is no longer a bare type parameter.
This technique proves essential when you need to perform whole-union checks rather than individual member checks, which is particularly useful when building type-safe API integrations that require precise type matching across complex data structures.
1// Preventing distribution with tuple wrapping2type IsString2<T> = [T] extends [string] ? 'yes' : 'no';3 4// Different results with and without distribution5type Dist1 = IsString1<string | number>; // 'yes' | 'no' (distributed)6type Dist2 = IsString2<string | number>; // 'no' (not distributed)The infer Keyword for Type Extraction
Introduction to Type Inference
The infer keyword extends the power of conditional types by allowing you to extract and capture parts of compound types. While conditional types let you choose between two type branches based on a condition, infer lets you name and use type information discovered during that condition evaluation. This capability transforms conditional types from simple branching logic into powerful type transformation tools capable of decomposing complex types into their constituent parts.
The infer keyword can only appear within the extends clause of a conditional type, where it declares a type variable that TypeScript will attempt to infer based on the structure of the type being checked. For example, when you want to extract the element type from an array, you use infer to capture the type of elements the array contains. The conditional type checks whether the input type extends Array<infer E> and if successful, E becomes that element type.
1// Extracting array element types2type ElemType<Arr> = Arr extends Array<infer Elem> ? Elem : never;3 4type StringArrayElem = ElemType<Array<string>>; // string5type NumberArrayElem = ElemType<Array<number>>; // number6 7// Extracting Promise resolution types8type UnwrapPromise<P> = P extends Promise<infer T> ? T : P;9 10type PromiseResult = UnwrapPromise<Promise<number>>; // numberBuilt-in Utility Types Based on Conditional Types
The Exclude Utility Type
TypeScript's standard library includes several utility types built entirely on conditional types. The Exclude<T, U> utility type removes from type T any types that are assignable to type U, using a simple conditional type that returns never for excluded types and the type itself for retained types. Because conditional types distribute over unions, Exclude processes each union member independently, filtering out the unwanted types.
The implementation of Exclude demonstrates the elegance of conditional types for type-level operations. By checking whether each member of T extends U, the type system identifies which members should be removed. Returning never for those members causes them to vanish from the resulting union, while returning the original type for retained members preserves them in the output.
1// Exclude removes types from union2type Exclude<T, U> = T extends U ? never : T;3 4type Union = 1 | 'a' | 2 | 'b';5type NoNumbers = Exclude<Union, number>; // 'a' | 'b'6type NoStrings = Exclude<Union, string>; // 1 | 27 8// Extract keeps only matching types9type Extract<T, U> = T extends U ? T : never;10 11type OnlyNumbers = Extract<Union, number>; // 1 | 2Exclude<T, U>
Removes from T any types assignable to U
Extract<T, U>
Keeps from T only types assignable to U
NonNullable<T>
Removes null and undefined from a type
ReturnType<T>
Extracts the return type of a function
Parameters<T>
Extracts parameter types as a tuple
InstanceType<T>
Gets the instance type of a constructor
Practical Use Cases
Type-Safe API Responses
Handle success and error responses with precise types using conditional types to distinguish between response states.
Discriminated Union Processing
Filter and transform discriminated union members based on their discriminator properties.
Generic Constraints
Create flexible generic APIs that adapt to different input type structures using conditional type constraints.
Type Validation
Build type-level validators that check complex type relationships and enforce constraints at compile time.
1// API Response type with conditional types2type ApiResponse<T> =3 T extends { ok: true } ? { data: T } :4 T extends { error: string } ? { error: T['error'] } :5 never;6 7// Usage8type SuccessResponse = ApiResponse<{ ok: true; data: string }>;9// { data: { ok: true; data: string } }10 11type ErrorResponse = ApiResponse<{ error: 'Not found' }>;12// { error: 'Not found' }Frequently Asked Questions
Conclusion
Conditional types represent one of TypeScript's most powerful features for creating sophisticated type-level abstractions. By understanding distributivity, the infer keyword, and the built-in utility types, you gain access to a powerful toolkit for creating type-safe, self-documenting code that catches errors at compile time.
Mastering conditional types opens the door to advanced TypeScript patterns that make your code more robust and maintainable. Whether you're building libraries, APIs, or applications, conditional types provide the foundation for type-level programming that adapts to your specific needs. These techniques are essential for professional web development projects that require strict type safety.
Start incorporating conditional types into your codebase today, and experience the benefits of compile-time type safety combined with flexible, expressive type definitions.