← TypeScript Thực Chiến

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 | BB | 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ỏ nullundefined 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 là phần nâng cao - bạn không cần tự viết nó mỗi ngày. Phần lớn thời gian mèo con sẽ dùng các utility type có sẵn (bài kế), vốn được dựng từ đúng những mảnh này. Hiểu được cơ chế giúp bạn đọc thông báo lỗi và tự sửa khi cần.

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

Bài sau học mapped type và template literal type - cách duyệt qua từng khoá của một kiểu để tạo kiểu mới, và ghép chuỗi ở tầng kiểu. Cùng với conditional, đây là bộ ba dựng nên mọi utility type.

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.

Phần lớn code ứng dụng dùng các utility type có sẵn (Bài tới) vốn được dựng từ conditional + infer. Bạn cần tự viết khi làm thư viện, hoặc khi cần một kiểu "biến hình" theo đầu vào mà chưa có sẵn. Đọc hiểu được chúng đã là một bước lớn.

never là "không có giá trị nào", nên nó không thêm khả năng gì vào một union: string | never chính là string. Tính chất này rất hữu ích - như ở Bước 4, cho null/undefined hoá thành never rồi để chúng tự rụng khỏi union.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Conditional type T extends U ? X : Y làm gì?

Bài tập về nhà

  1. 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. 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. 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. 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. 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. 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.