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.
Conditional type là biểu thức điều kiện ở cấp độ kiểu. Cú pháp T extends U ? X : Y có nghĩa là: nếu T có thể gán cho U, kết quả là kiểu X, ngược lại là kiểu Y. Đây là if/else của hệ thống kiểu TypeScript.
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.
extends trong conditional type kiểm tra tính gán được (assignability), không phải sự bằng nhau về kiểu. Vì vậy string literal extends string, never extends anything, và unknown chỉ extends unknown | any.
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.
Khi T là một tham số kiểu trần (không được bọc trong tuple hay object), conditional type sẽ tự động phân phối trên union. TypeScript áp dụng điều kiện cho từng thành viên của union rồi hợp nhất kết quả lại.
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 typeT is a bare type parameter. Wrapping T in [T] or { v: T } disables distribution — a deliberate escape hatch.Tính phân phối chỉ xảy ra khi T là tham số kiểu trần. Nếu bạn bọc T trong [T] hoặc { v: T }, tính phân phối bị tắt. Đây là kỹ thuật quan trọng để kiểm soát hành vi.
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.
Có những tình huống bạn muốn kiểm tra toàn bộ union như một đơn vị, không phải từng thành viên riêng lẻ. Bọc cả hai vế trong một tuple một phần tử sẽ tắt tính phân phối.
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;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.
Từ khóa infer cho phép bạn khai báo một biến kiểu trong phần điều kiện và 'bắt' (capture) một phần của kiểu đang được kiểm tra. Đây là công cụ mạnh nhất trong conditional types.
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;12TypeScript ships a set of utility types built on conditional types. Understanding their implementations lets you craft custom variants.
TypeScript cung cấp nhiều utility types được xây dựng bằng conditional types. Hiểu cách chúng được triển khai giúp bạn tự tạo ra các biến thể tùy chỉnh.
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;Conditional types nest naturally to create multi-branch type logic — the type-level equivalent of a switch statement.
Conditional types có thể lồng nhau để tạo ra logic phân nhánh phức tạp. Điều này đặc biệt hữu ích khi xây dựng các type-level switch statements.
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"12Conditional types đệ quy có thể gây ra lỗi 'Type instantiation is excessively deep'. Để tránh điều này, hãy thêm một điều kiện cơ sở rõ ràng hoặc giới hạn độ sâu đệ quy bằng tuple counting.
Because of distribution, conditional types are the standard way to filter union members — returning never for unwanted members effectively removes them from the union.
Vì tính phân phối, bạn có thể dùng conditional types để lọc các thành viên không mong muốn ra khỏi một union — đây là pattern cực kỳ hữu ích trong thực tế.
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 }T extends U ? X : Y là cú pháp cơ bản của conditional type
T is a bare type parameterConditional types phân phối tự động trên union khi T là tham số kiểu trần
[T] extends [U] to disable distributionBọc trong [T] extends [U] để vô hiệu hóa tính phân phối
infer captures a sub-type from within the condition branchinfer cho phép bắt và đặt tên cho phần của kiểu bên trong điều kiện
NonNullable, ReturnType, Parameters đều được xây dựng bằng conditional types
Lồng conditional types để tạo type-level switch statements
never in the false branch to filter unwanted union membersDùng never trong nhánh false để lọc thành viên không mong muốn khỏi union