Skip to content
Skip to content
DocsTypeScript ExpertexpertConditional Types
Chapter 2 of 10·expert·10 min read

Conditional Types

Kiểu Điều Kiện

Type-level if/else expressions that unlock advanced type transformations

Hover or tap any paragraph to see Vietnamese translation

Basic Syntax

A conditional type is an if/else expression at the type level. The syntax T extends U ? X : Y resolves to type X when T is assignable to U, and to type Y otherwise. It is the type system's equivalent of a ternary operator.

basic-conditional.ts
1type IsString<T> = T extends string ? true : false;23type A = IsString<string>;   // true4type B = IsString<number>;   // false5type C = IsString<"hello">;  // true — "hello" extends string67// The condition checks assignability, not identity8type D = IsString<string | number>; // boolean (distributive — see next section)

The extends check tests assignability, not identity. String literals extend string, never extends everything, and unknown only extends unknown and any.

Distributive Conditional Types

When T is a bare type parameter (not wrapped in a tuple or object), a conditional type automatically distributes over a union. TypeScript applies the condition to each union member independently, then unions the results.

distributive.ts
1type ToArray<T> = T extends unknown ? T[] : never;23// With a union, each member is processed separately:4type R = ToArray<string | number>;5// = ToArray<string> | ToArray<number>6// = string[] | number[]78// Without distribution you would get:9type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;10type S = ToArrayNonDist<string | number>;11// = (string | number)[]  — the whole union becomes one array type
Info
Distribution only triggers when T is a bare type parameter. Wrapping T in [T] or { v: T } disables distribution — a deliberate escape hatch.

Non-Distributive Conditional Types

Sometimes you want to test the entire union as a unit rather than member by member. Wrapping both sides in a single-element tuple disables distribution.

non-distributive.ts
1// Distributive — resolves to never | never = never ... except for never itself2type IsNever<T> = T extends never ? true : false;3type Bad = IsNever<never>; // never  (distributes over empty union = never)45// Non-distributive — correctly checks for never6type IsNeverSafe<T> = [T] extends [never] ? true : false;7type Good = IsNeverSafe<never>; // true8type Also = IsNeverSafe<string>; // false910// Another common use: check if a type is exactly unknown11type IsUnknown<T> = [T] extends [unknown]12	? [unknown] extends [T]13		? true14		: false15	: false;

Inferring Types with infer

The infer keyword lets you declare a type variable inside the condition branch and capture a portion of the type being tested. It is the most powerful feature of conditional types.

infer-keyword.ts
1// Extract the return type of a function2type ReturnType<T> = T extends (...args: unknown[]) => infer R ? R : never;34type F1 = () => string;5type F2 = (x: number) => boolean[];67type R1 = ReturnType<F1>; // string8type R2 = ReturnType<F2>; // boolean[]910// Extract element type from an array11type ElementType<T> = T extends (infer E)[] ? E : T;12

Practical Built-in Utility Types

TypeScript ships a set of utility types built on conditional types. Understanding their implementations lets you craft custom variants.

built-in-utility-types.ts
1// NonNullable<T> — remove null and undefined from a union2type NonNullable<T> = T extends null | undefined ? never : T;3type NN = NonNullable<string | null | undefined>; // string45// ReturnType<T> — extract the return type of a function6type ReturnType<T extends (...args: unknown[]) => unknown> =7	T extends (...args: unknown[]) => infer R ? R : never;8type RT = ReturnType<() => Promise<number>>; // Promise<number>910// Parameters<T> — extract parameter types as a tuple11type Parameters<T extends (...args: unknown[]) => unknown> =12	T extends (...args: infer P) => unknown ? P : never;

Nested Conditional Types

Conditional types nest naturally to create multi-branch type logic — the type-level equivalent of a switch statement.

nested-conditional.ts
1type TypeName<T> =2	T extends string ? "string" :3	T extends number ? "number" :4	T extends boolean ? "boolean" :5	T extends undefined ? "undefined" :6	T extends Function ? "function" :7	"object";89type TN1 = TypeName<string>;    // "string"10type TN2 = TypeName<() => void>; // "function"11type TN3 = TypeName<object>;    // "object"12
Info
Recursive conditional types can hit TypeScript's "Type instantiation is excessively deep" limit. Always provide a clear base case and test with deeply nested structures.

Filtering Unions

Because of distribution, conditional types are the standard way to filter union members — returning never for unwanted members effectively removes them from the union.

filter-unions.ts
1// Keep only members assignable to Filter2type Extract<T, Filter> = T extends Filter ? T : never;34// Remove members assignable to Exclude5type Exclude<T, Exclude> = T extends Exclude ? never : T;67type Strings = Extract<string | number | boolean, string>; // string8type NoNums = Exclude<string | number | boolean, number>;  // string | boolean910// Filter object union by a discriminant11type Action =12	| { type: "add"; payload: string }13	| { type: "remove"; id: number }14	| { type: "reset" };1516type FindByType<Union, Type extends string> =17	Extract<Union, { type: Type }>;1819type AddAction = FindByType<Action, "add">;20// { type: "add"; payload: string }

Summary

  • T extends U ? X : Y is the fundamental conditional type syntax
  • Conditional types distribute over unions when T is a bare type parameter
  • Wrap in [T] extends [U] to disable distribution
  • infer captures a sub-type from within the condition branch
  • NonNullable<T>, ReturnType<T>, and Parameters<T> are all built on conditional types
  • Nest conditional types to build type-level switch expressions
  • Return never in the false branch to filter unwanted union members
  • Built: 6/25/2026, 3:03:36 PM