Conditional Type T extends U ? X : Y
| Since: | TypeScript 2.8(2018) |
|---|
A feature for conditional branching at the type level. You can write a conditional expression as a type: "If type T extends type U, use type X; otherwise, use type Y." It is used to build advanced utility types.
Syntax
type TypeName = T extends U ? X : Y; // infer keyword: infers and extracts part of a type. type TypeName = T extends SomeType<infer R> ? R : never;
Syntax list
| Syntax | Description |
|---|---|
| T extends U ? X : Y | The basic form of a conditional type. Resolves to type X if T is assignable to U, otherwise resolves to type Y. |
| infer R | Infers a type within a conditional type and binds it to a variable (R). Used to extract part of a type. |
| Distributive conditional types | A behavior where, when a union type is passed as a type parameter, the conditional type is applied to each member of the union individually and the results are combined into a union. |
| ReturnType<T> | A built-in utility type that uses infer to extract the return type of function type T. |
| Parameters<T> | A built-in utility type that uses infer to extract the parameter types of function type T as a tuple. |
Sample code
type IsString<T> = T extends string ? true : false; type A = IsString<string>; // true type B = IsString<number>; // false type IsArray<T> = T extends any[] ? true : false; type C = IsArray; // true type D = IsArray<string>; // false // infer keyword: extracts part of a type. // A utility type that extracts the element type from an array type. type ElementType<T> = T extends (infer E)[] ? E : never; type NumElement = ElementType ; // number type StrElement = ElementType ; // string // The built-in ReturnType is also implemented using infer. type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never; function greet(name: string): string { return `Hello, ${name}!`; } type GreetReturn = MyReturnType<typeof greet>; // string console.log("Return type: string"); // The return type of greet is string. // Distributive conditional types: the condition is applied to each member of a union type. type NonNullable<T> = T extends null | undefined ? never : T; // Applying NonNullable to string | null | undefined. type Cleaned = NonNullable<string | null | undefined>; // string // How to disable distribution: wrap the type parameter in a tuple. type IsStringExact<T> = [T] extends [string] ? true : false; type E = IsStringExact<string | number>; // false (not distributed) // Exclude: removes a type from a union type. type MyExclude<T, U> = T extends U ? never : T; type Excluded = MyExclude<"a" | "b" | "c", "b">; // "a" | "c" console.log("Excluded type: 'a' | 'c'");
Running the above produces the following output:
npx ts-node ts_conditional_type.ts Return type: string Excluded type: 'a' | 'c'
Practical patterns (how built-in utility types work)
Many of TypeScript's built-in utility types are implemented using conditional types and infer. Understanding the mechanics lets you build your own utility types.
type MyAwaited<T> = T extends Promise<infer R> ? MyAwaited<R> : T; type A = MyAwaited>; // string // DeepReadonly: recursively makes all properties readonly type DeepReadonly<T> = T extends object ? { readonly [K in keyof T]: DeepReadonly } : T; interface Config { host: string; options: { port: number; ssl: boolean }; } type ReadonlyConfig = DeepReadonly<Config>; // Trying to mutate ReadonlyConfig.options.port causes a compile error // PickByValue: extract only properties whose value matches a given type type PickByValue<T, V> = { [K in keyof T as T[K] extends V ? K : never]: T[K]; }; interface UserProfile { id: number; name: string; email: string; age: number; } type StringFields = PickByValue<UserProfile, string>; // { name: string; email: string } // FunctionKeys: extract the names of properties that are functions type FunctionKeys<T> = { [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never; }[keyof T]; interface Character { name: string; power: number; attack: () => void; defend: (value: number) => boolean; } type CharacterMethods = FunctionKeys<Character>; // "attack" | "defend" console.log("Conditional type patterns demo (types are erased at runtime)");
Running the above produces the following output:
npx ts-node sample_conditional_patterns.ts Conditional type patterns demo (types are erased at runtime)
Common Mistakes
Common mistake 1: unintended distributive conditional types
Distributive conditional types are applied unintentionally. If you wanted (string | number)[], the distribution is unintended here.
type ToArray<T> = T extends any ? T[] : never; type B1 = ToArray<string | number>; // string[] | number[] (distributed!)
Fix: wrap the type parameter in a tuple to prevent distribution.
type ToArraySafe<T> = [T] extends [any] ? T[] : never; type B2 = ToArraySafe<string | number>; // (string | number)[]
Common mistake 2: deeply nested infer
Nesting infer too deeply hurts readability. Split into multiple named types for better maintainability.
type X = T extends Promise<infer A extends Array<infer B>> ? B : never;
Common mistake 3: never distribution behavior
Forgetting that never distributes to nothing. When never is passed as a union, it distributes into nothing before the check runs.
type IsNever<T> = T extends never ? true : false; type C1 = IsNever<never>; // never (not true — never distributes away!)
Fix: wrap in a tuple to check correctly.
type IsNeverSafe<T> = [T] extends [never] ? true : false;
type C2 = IsNeverSafe<never>; // true
console.log("Common mistakes demo (type-level only, no runtime output)");
Running the above produces the following output:
npx ts-node sample_conditional_mistakes.ts Common mistakes demo (type-level only, no runtime output)
Overview
Conditional types use a ternary-like syntax — T extends U ? X : Y — to implement conditional branching at the type level. It means "if T is assignable to U, use type X; otherwise, use type Y." They are primarily used in combination with generics to create utility types whose output type changes dynamically based on the input type.
The infer keyword can only be used inside a conditional type, and it captures (infers) part of a type as a variable. Many of TypeScript's built-in utility types — such as ReturnType<T>, which extracts a function's return type, and Awaited<T>, which unwraps a Promise's resolved type — are implemented using infer.
Distributive conditional types are a behavior where, when a union type is passed as a type parameter, the conditional type is applied to each member of the union individually. Types like NonNullable<T> and Exclude<T, U> rely on this behavior. To prevent distribution, wrap the type parameter in a tuple and write [T] extends [U].
If you find any errors or copyright issues, please contact us.