Conditional Types
Conditional types let you choose between two types based on a condition evaluated at the type level. The syntax mirrors JavaScript's ternary operator — but it runs entirely at compile time, making your types adaptive and expressive.
Basic Syntax
The fundamental form is:
T extends U ? X : Y
Read it as: "If T is assignable to U, resolve to X; otherwise resolve to Y."
type IsString<T> = T extends string ? true : false; type A = IsString<string>; // true type B = IsString<number>; // false type C = IsString<"hello">; // true (string literal extends string) type D = IsString<42>; // false
extends here is a type constraint check, not class inheritance. It asks: "Is T assignable to U?"How Conditional Types Are Evaluated
TypeScript resolves conditional types by substituting the actual type argument for
T and checking whether it satisfies the constraint U. If T is a concrete type
at the time of evaluation, the result is immediate.
If T is still a generic type parameter (not yet resolved), TypeScript defers
the evaluation until it has more information — typically when the generic is
instantiated at a call site.
// Immediate evaluation — T is known
type Result1 = string extends string | number ? "yes" : "no"; // "yes"
type Result2 = boolean extends string ? "yes" : "no"; // "no"
// Deferred evaluation — T is still generic inside the function
function process<T>(value: T): T extends string ? "str" : "other" {
// TypeScript defers resolution here, so a cast is needed
return (typeof value === "string" ? "str" : "other") as any;
}
const r1 = process("hello"); // "str"
const r2 = process(42); // "other"Distributive Conditional Types
When you apply a conditional type to a naked type parameter (a plain generic, not wrapped in anything), TypeScript automatically distributes the condition over each member of a union. This is one of the most powerful — and sometimes surprising — behaviours in the type system.
type ToArray<T> = T extends any ? T[] : never; // With a union, distribution kicks in automatically: type Result = ToArray<string | number>; // Step 1: ToArray<string> | ToArray<number> // Step 2: string[] | number[] type A = ToArray<string>; // string[] type B = ToArray<string | number>; // string[] | number[] type C = ToArray<boolean>; // false[] | true[] (boolean = false | true)
Non-Distributive Conditional Types
Sometimes you want to treat the union as a whole rather than distributing over its
members. Wrap T (and U) in a single-element tuple [T] on both sides to opt out
of distribution.
// Distributive — splits the union, returns boolean (true | false) type IsString_D<T> = T extends string ? true : false; type D = IsString_D<string | number>; // boolean // Non-distributive — treats union as a single unit type IsString_ND<T> = [T] extends [string] ? true : false; type ND = IsString_ND<string | number>; // false // Another example: check if T is exactly string (not a union containing string) type IsExactlyString<T> = [T] extends [string] ? ([string] extends [T] ? true : false) : false; type X1 = IsExactlyString<string>; // true type X2 = IsExactlyString<string | number>; // false type X3 = IsExactlyString<"hello">; // false (literal, not exactly string)
Practical Examples — Type Predicates
Here are several reusable conditional types you will encounter or write in real codebases:
// Is T exactly never? type IsNever<T> = [T] extends [never] ? true : false; type A = IsNever<never>; // true type B = IsNever<string>; // false // Is T a mutable array? type IsArray<T> = T extends any[] ? true : false; type C = IsArray<number[]>; // true type D = IsArray<readonly string[]>; // false // Extend to include readonly arrays type IsArrayLike<T> = T extends readonly any[] ? true : false; type E = IsArrayLike<readonly string[]>; // true // Is T a function? type IsFunction<T> = T extends (...args: any[]) => any ? true : false; type F = IsFunction<() => void>; // true type G = IsFunction<(x: string) => number>; // true type H = IsFunction<string>; // false // Is T a Promise? type IsPromise<T> = T extends Promise<any> ? true : false; type I = IsPromise<Promise<string>>; // true type J = IsPromise<string>; // false
Extracting and Excluding with Conditional Types
TypeScript's built-in Extract and Exclude utility types are both implemented
using conditional types under the hood. Knowing this helps you understand how to
build your own type-level filters.
// Built-in implementations (simplified) type Extract<T, U> = T extends U ? T : never; type Exclude<T, U> = T extends U ? never : T; type Primitives = string | number | boolean | object | symbol; // Keep only string and number type StrOrNum = Extract<Primitives, string | number>; // string | number // Remove object type NoObjects = Exclude<Primitives, object>; // string | number | boolean | symbol // Custom: keep only nullable members of a union type OnlyNullable<T> = T extends null | undefined ? T : never; type N = OnlyNullable<string | null | undefined | number>; // null | undefined // Custom: remove functions from a type type NonFunction<T> = T extends (...args: any[]) => any ? never : T; type M = NonFunction<string | (() => void) | number>; // string | number
Nested Conditional Types
Conditional types can be chained to build more complex type logic — just like nested ternaries in JavaScript, but at the type level.
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends null ? "null" :
T extends Function ? "function" :
"object";
type T1 = TypeName<string>; // "string"
type T2 = TypeName<42>; // "number"
type T3 = TypeName<true>; // "boolean"
type T4 = TypeName<() => void>; // "function"
type T5 = TypeName<{ a: 1 }>; // "object"
type T6 = TypeName<null>; // "null"Combining with infer (Preview)
The real power of conditional types emerges when combined with the infer keyword,
which lets you capture a piece of a type during the extends check and use it in
the true branch. The infer keyword is covered in depth in its own page — here is a
quick preview:
// Extract the return type of any function type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never; type Fn = (x: number) => string; type R = MyReturnType<Fn>; // string // Unwrap a Promise one level type Unwrap<T> = T extends Promise<infer V> ? V : T; type P = Unwrap<Promise<number>>; // number type Q = Unwrap<string>; // string (not a Promise, returns T as-is) // Extract the first element type of a tuple type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never; type H1 = Head<[string, number, boolean]>; // string type H2 = Head<[]>; // never
infer keyword only works inside a conditional type's extends clause. You cannot use infer outside of a T extends ... ? ... : ... expression.Common Patterns at a Glance
Pattern | Code | Use case |
|---|---|---|
Type guard flag | T extends X ? true : false | Boolean indicator for a type |
Type filter | T extends X ? T : never | Keep only matching members |
Type remover | T extends X ? never : T | Remove matching members |
Non-distributive check | [T] extends [X] ? … : … | Treat union as a whole |
Type extractor | T extends Wrap<infer U> ? U : never | Unwrap a wrapper type |
Real-World Example — DeepReadonly
Combining conditional types with recursion lets you build utilities that operate at
any nesting depth. Here is a DeepReadonly type that marks every nested property
and array element as readonly:
type DeepReadonly<T> =
T extends (infer U)[]
? ReadonlyArray<DeepReadonly<U>>
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
interface Config {
server: {
host: string;
ports: number[];
};
debug: boolean;
}
type FrozenConfig = DeepReadonly<Config>;
// {
// readonly server: {
// readonly host: string;
// readonly ports: ReadonlyArray<number>;
// };
// readonly debug: boolean;
// }
const cfg: FrozenConfig = {
server: { host: "localhost", ports: [3000, 8080] },
debug: true,
};
// cfg.debug = false; // Error: cannot assign to 'debug' (readonly)
// cfg.server.ports.push(9000); // Error: push does not exist on ReadonlyArrayinfer plugs in. These are the building blocks for almost every advanced TypeScript utility type.