Menu

Thu hẹp kiểu trong TypeScript: phân tích luồng điều khiển

Thu hẹp kiểu là cách TypeScript biến một kiểu rộng như string | number thành một kiểu cụ thể bên trong if, switch hay sau một lệnh return sớm. Mọi dạng thu hẹp ở cùng một chỗ: typeof, truthiness, so sánh bằng, in, instanceof, phép gán, type predicate và discriminated union.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

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ạngVí dụThu hẹp
typeoftypeof x === "string"kiểu nguyên thủy và hàm
Truthinessif (x)loại bỏ null, undefined và các literal falsy
So sánh bằngx === "a", x == null, x !== undefinedliteral, null, undefined
in"swim" in petunion đối tượng, theo thuộc tính
instanceoferr instanceof TypeErrorinstance của class
Array.isArrayArray.isArray(x)mảng và mọi thứ khác
Phép gánx = 5thành kiểu được gán
Type predicatefunction isUser(x: unknown): x is Userbất cứ thứ gì bạn kiểm tra được
Hàm assertionfunction assertUser(x: unknown): asserts x is Usermọi thứ sau lời gọi
Thuộc tính phân biệtswitch (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.name sẽ thu hẹp obj.name (và obj["name"]), nhưng không thu hẹp obj[key] khi key là một biến string chứ 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ột const hoặ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): boolean khô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).

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu