Bài 7 · Nâng cao · 26 phút
Conditional types & infer
Biên soạn bởi Nguyễn Anh Tuấn
Lập trình ở tầng kiểu: conditional type (T extends U ? X : Y) như câu lệnh if cho kiểu; từ khoá infer để rút một kiểu con ra; conditional phân phối (distributive) trên union. Bước TypeScript trở thành một ngôn ngữ ở tầng kiểu.
Tới giờ kiểu của ta khá "tĩnh". Conditional type cho kiểu biết rẽ nhánh:
cú pháp T extends U ? X : Y đọc là "nếu T khớp U thì kiểu là X, không
thì là Y" - đúng một câu if, nhưng chạy trên KIỂU lúc biên dịch:
conditional-type.ts
type Loai<T> = T extends string ? "chuoi" : T extends number ? "so" : "khac";
type A = Loai<string>; // "chuoi"
type B = Loai<number>; // "so"
type C = Loai<boolean>; // "khac" Bấm thử từng kiểu đầu vào để xem conditional rẽ nhánh ra sao:
type Loai<T> = T extends string ? "chuoi" : T extends number ? "so" : "khac"
Chọn kiểu T đầu vào:
Loai<string> = "chuoi"
string extends string nên lấy nhánh đúng đầu tiên.
- ▸Conditional type T extends U ? X : Y chọn nhánh dựa trên kiểu đầu vào.
- ▸Nó chạy lúc biên dịch trên kiểu, không sinh code JavaScript nào.
- ▸Có thể nối nhiều conditional thành chuỗi như if / else if / else.
Sức mạnh thật mở ra với infer: đặt một biến kiểu tạm ngay trong điều
kiện để TypeScript tự điền rồi cho bạn dùng. Vd lấy kiểu phần tử của một mảng, hay kiểu trả về của
một hàm:
infer.ts
// Lay kieu phan tu cua mang
type PhanTu<T> = T extends Array<infer E> ? E : never;
type A = PhanTu<number[]>; // number
type B = PhanTu<string[]>; // string
// Lay kieu TRA VE cua mot ham (chinh la ReturnType co san)
type KetQua<T> = T extends (...args: any[]) => infer R ? R : never;
type C = KetQua<() => boolean>; // boolean infer = đặt tên cho phần chưa biết
Array<infer E> nói "nếu T là một mảng nào đó, hãy gọi kiểu phần tử là E". TypeScript
khớp mẫu và điền E giúp bạn. Đây chính là cách các kiểu có sẵn như ReturnType, Parameters, Awaited được dựng nên.Một hành vi quan trọng: khi đầu vào là union, conditional type phân phối qua từng thành viên rồi gộp kết quả lại:
distributive.ts
type ToArray<T> = T extends any ? T[] : never;
type X = ToArray<string | number>;
// = string[] | number[] (KHONG phai (string | number)[])
// vi no tach thanh ToArray<string> | ToArray<number> Mèo con thấy lại điều này ở widget Bước 1: Loai<string | number> cho ra "chuoi" | "so", vì TS xử lý từng nhánh string và number riêng.
(Thứ tự hiển thị các thành viên union có thể khác nhau - A | B và B | A là cùng một kiểu.)
- ▸Conditional trên một union sẽ tách ra áp cho từng thành viên rồi gộp kết quả.
- ▸ToArray<string | number> ra string[] | number[], không phải (string | number)[].
- ▸Muốn xem union như một khối thì bọc tuple [T] extends [U] để tắt phân phối.
Gộp conditional, infer và phân phối, ta dựng được những helper hữu ích. Ví dụ kinh điển: loại bỏ null và undefined khỏi một kiểu,
tận dụng việc never tự rụng khỏi union:
khong-rong.ts
type KhongRong<T> = T extends null | undefined ? never : T;
type Y = KhongRong<string | null | undefined>;
// = string
// phan phoi: string -> string, null -> never, undefined -> never;
// roi never tu rung khoi union, chi con string Trung thực
Conditional type cho kiểu khả năng rẽ nhánh, infer cho khả năng bóc tách, và phân phối cho khả năng trải qua union. Đây là lúc generics thật sự thành một ngôn ngữ ở tầng kiểu. Bài sau thêm hai công cụ nữa để SINH kiểu từ kiểu.
Bài tiếp theo: Mapped & template literal types
Câu hỏi thường gặp
if chạy trên GIÁ TRỊ lúc runtime; conditional type chạy trên KIỂU lúc biên dịch rồi bị xoá (type erasure). Nó không sinh ra một dòng JavaScript nào - chỉ giúp TypeScript suy ra kiểu kết quả tuỳ theo kiểu đầu vào.
infer đặt một "biến kiểu tạm" ngay trong điều kiện để TypeScript tự điền vào rồi cho bạn dùng ở nhánh đúng. T extends Array<infer E> ? E : never nghĩa là "nếu T là một mảng, hãy gọi kiểu phần tử của nó là E và trả về E". Đó là cách bóc một kiểu con ra khỏi một kiểu lớn.
Thường là tiện: áp một conditional lên string | number tự động cho kết quả trên từng nhánh. Nhưng đôi khi bạn muốn xem cả union như MỘT khối - khi đó bọc trong tuple [T] extends [U] để tắt phân phối. Mặc định cứ nhớ: conditional trải qua từng thành viên của union.
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
Conditional type T extends U ? X : Y làm gì?
- 1
Đọc một conditional
Tự viết type Loai<T> = T extends string ? "chuoi" : T extends number ? "so" : "khac". Dùng widget ở Bước 1 (hoặc Playground) kiểm kết quả cho 4 kiểu đầu vào khác nhau.
✅ Hoàn thành khi: Bốn dòng: đầu vào và kết quả Loai tương ứng, có ít nhất một literal và một union.
- 2
Bóc kiểu bằng infer
Viết type PhanTu<T> = T extends Array<infer E> ? E : never. Áp lên number[] và string[], soi kiểu rút ra.
✅ Hoàn thành khi: PhanTu<number[]> ra number, PhanTu<string[]> ra string; kèm một câu giải thích infer làm gì.
- 3
Rút kiểu trả về
Viết một conditional dùng infer để lấy kiểu TRẢ VỀ của một hàm (gợi ý: T extends (...args: any[]) => infer R ? R : never). Thử với một hàm trả boolean.
✅ Hoàn thành khi: Áp lên () => boolean ra boolean; nhận xét đây chính là cách ReturnType có sẵn hoạt động.
- 4
Quan sát phân phối
Viết type ToArray<T> = T extends any ? T[] : never. So kết quả của ToArray<string | number> với (string | number)[].
✅ Hoàn thành khi: Ghi rõ ToArray<string | number> ra string[] | number[] (phân phối), khác với (string | number)[].
- 5
Tự dựng KhongRong
Viết type KhongRong<T> = T extends null | undefined ? never : T. Áp lên string | null | undefined.
✅ Hoàn thành khi: Kết quả ra string; giải thích vì sao null/undefined hoá never rồi rụng khỏi union.
- 6
Dự đoán trước khi chạy
Tự nghĩ một conditional type mới và 3 đầu vào. TRƯỚC khi kiểm, viết ra kết quả mèo con dự đoán, rồi đối chiếu bằng Playground.
✅ Hoàn thành khi: Bảng 3 dòng dự đoán đặt cạnh kết quả thật, kèm ghi chú chỗ nào đoán sai và vì sao.