Thu hẹp kiểu (type narrowing) là việc TypeScript tìm ra một kiểu cụ thể hơn cho một giá trị tại một điểm trong code, dựa trên những phép kiểm tra mà code đã thực hiện. Một tham số string | number trở thành string bên trong if (typeof x === "string") và thành number trong else.
Gọi value.toFixed(2) trước phép kiểm tra sẽ là lỗi biên dịch, vì toFixed không tồn tại trên string. Phép kiểm tra là JavaScript bình thường và chạy lúc chạy; việc thu hẹp là trình biên dịch đọc phép kiểm tra đó và điều chỉnh kiểu. Không có gì được xuất thêm.
Phân tích luồng điều khiển
TypeScript theo dõi mọi đường đi qua một hàm: if/else, return và throw sớm, switch, vòng lặp, và các toán tử ngắn mạch &&, ||, ?? và ?:. Tại mỗi điểm, kiểu của một biến là những gì vẫn còn có thể xảy ra ở đó.
Kiểu viết return sớm ("guard clause") là cách thu hẹp dễ đọc nhất: xử lý các trường hợp đặc biệt trước, rồi phần còn lại của hàm làm việc với kiểu đã gọn gàng.
Mọi cách thu hẹp kiểu
| Dạng | Ví dụ | Thu hẹp |
|---|---|---|
| typeof | typeof x === "string" | kiểu nguyên thủy và hàm |
| Truthiness | if (x) | loại bỏ null, undefined và các literal falsy |
| So sánh bằng | x === "a", x == null, x !== undefined | literal, null, undefined |
in | "swim" in pet | union đối tượng, theo thuộc tính |
| instanceof | err instanceof TypeError | instance của class |
Array.isArray | Array.isArray(x) | mảng và mọi thứ khác |
| Phép gán | x = 5 | thành kiểu được gán |
| Type predicate | function isUser(x: unknown): x is User | bất cứ thứ gì bạn kiểm tra được |
| Hàm assertion | function assertUser(x: unknown): asserts x is User | mọi thứ sau lời gọi |
| Thuộc tính phân biệt | switch (shape.kind) | tagged union |
Ba dạng cuối được trình bày trong trang type guard và discriminated union. Các dạng còn lại ở bên dưới.
Thu hẹp theo truthiness
if (x) loại bỏ null và undefined (cùng các literal type false, 0, ""). Cách này ngắn gọn, nhưng có một cái bẫy kinh điển: 0 và "" là falsy, nên những giá trị hợp lệ bị coi là không có.
Với số và chuỗi, hãy so sánh tường minh với undefined hoặc null (hoặc dùng ??). Truthiness dùng tốt cho đối tượng, mảng và hàm, vì chúng không bao giờ là falsy.
Thu hẹp theo so sánh bằng
===, !==, == và != thu hẹp kiểu ở cả hai vế. So sánh với một literal sẽ thu hẹp thành literal đó; == null (so sánh lỏng) khớp cả null lẫn undefined chỉ trong một phép kiểm tra, và đây là chỗ duy nhất mà so sánh lỏng được coi là cách viết chuẩn.
So sánh hai biến sẽ thu hẹp cả hai về phần kiểu chung mà chúng có thể có: nếu a: string | number và b: string | boolean thỏa a === b, cả hai đều là string bên trong if.
Toán tử in
"key" in obj thu hẹp một union các kiểu đối tượng về những thành viên có (hoặc có thể có) thuộc tính đó.
in cũng dùng được trên unknown khi bạn đã biết nó là đối tượng: sau typeof v === "object" && v !== null && "id" in v, TypeScript biết v có thuộc tính id kiểu unknown. Với những union bạn tự thiết kế, một thuộc tính nhãn chung (kind: "fish") rõ ràng hơn việc dò xem có phương thức nào: mẫu đó gọi là discriminated union.
Thu hẹp theo phép gán
Một biến có kiểu được khai báo, và một kiểu đã thu hẹp đi theo các phép gán của nó. Gán một giá trị sẽ thu hẹp biến về kiểu của giá trị đó, trong giới hạn của kiểu đã khai báo.
Khi nào thu hẹp kiểu bị mất
Thu hẹp kiểu mang tính cục bộ và thận trọng. Một vài tình huống sẽ đặt lại nó:
- Một biểu thức khác. Kiểm tra
obj.namesẽ thu hẹpobj.name(vàobj["name"]), nhưng không thu hẹpobj[key]khikeylà một biếnstringchứ không phải literal, cũng không thu hẹp một bản sao tạo ra trước phép kiểm tra. - Callback và phép gán lại. Bên trong một callback, một
letđã được thu hẹp chỉ giữ được thu hẹp nếu nó không bị gán lại sau khi callback được tạo. Mộtconsthoặc một tham số không bao giờ bị gán lại thì vẫn giữ được thu hẹp. - Phép kiểm tra bị giấu trong hàm hỗ trợ. Một hàm
isString(x: unknown): booleankhông cho trình biên dịch biết gì cả. Cho nó kiểu trả về là type predicate,x is string, thì các lời gọi tới nó sẽ thu hẹp kiểu giống nhưtypeof.
Trình biên dịch báo index.ts(5,38): error TS18048: 'x' is possibly 'undefined'. Callback có thể chạy muộn hơn, sau x = undefined. Xóa phép gán cuối đó (hoặc sao chép giá trị vào một const bên trong if) thì code biên dịch được và in ra 5 hai lần.
Một hàm hỗ trợ trả về boolean có thể sửa bằng cách khai báo điều nó chứng minh được. Đó chính là type guard:
function isString(value: unknown): value is string {
return typeof value === "string";
}
Từ TypeScript 5.5, trình biên dịch tự suy ra những predicate như vậy cho các arrow function đơn giản, vì thế list.filter((x) => x !== undefined) giờ trả về một mảng không chứa undefined.
Câu hỏi thường gặp
Thu hẹp kiểu trong TypeScript là gì?
Thu hẹp kiểu (narrowing) là việc TypeScript làm rõ kiểu của một biến bên trong một khối lệnh dựa trên phép kiểm tra mà code thực hiện. Sau if (typeof x === "string"), một string | number chỉ còn là string bên trong if và chỉ còn number trong else. Trình biên dịch theo dõi if, else, return, switch, &&, || và ?: để tìm ra kiểu tại mỗi điểm, việc này gọi là phân tích luồng điều khiển (control flow analysis).
Vì sao TypeScript không thu hẹp kiểu của tôi?
Các nguyên nhân thường gặp: phép kiểm tra nằm trên một biểu thức khác với biểu thức bạn dùng (kiểm tra obj.a nhưng dùng obj[key] với key có kiểu string); giá trị là một let bị gán lại sau khi một callback được tạo, nên callback mất thu hẹp; hoặc phép kiểm tra bị giấu trong một hàm hỗ trợ trả về boolean thường thay vì type predicate x is T.
Thu hẹp kiểu có hoạt động lúc chạy không?
Các phép kiểm tra thì có: typeof, instanceof, in và === là JavaScript bình thường và được chạy. Còn bản thân việc thu hẹp chỉ có lúc biên dịch. TypeScript đọc các phép kiểm tra lúc chạy của bạn và điều chỉnh kiểu tĩnh cho khớp, không có gì được thêm vào JavaScript xuất ra.
Làm sao để thu hẹp kiểu unknown trong TypeScript?
Bằng chính những phép kiểm tra đó: typeof value === "string", Array.isArray(value), value instanceof Date, hoặc với đối tượng là typeof value === "object" && value !== null && "id" in value. Với phép kiểm tra cần dùng lại, hãy viết một hàm type guard có kiểu trả về value is T.
Làm sao để lọc undefined khỏi một mảng trong TypeScript?
items.filter((x) => x !== undefined) trả về T[] không có undefined kể từ TypeScript 5.5, phiên bản suy ra callback là một type predicate. Với các phiên bản cũ hơn, hãy tự viết predicate: items.filter((x): x is T => x !== undefined).