Một union type liệt kê các lựa chọn bằng |: một giá trị kiểu string | number là chuỗi hoặc là số. Union là cách TypeScript mô tả những giá trị hợp lệ ở nhiều dạng khác nhau, và trình biên dịch buộc bạn kiểm tra xem mình đang có dạng nào trước khi dùng bất cứ thứ gì riêng của dạng đó.
Bên trong mỗi nhánh của phép kiểm tra typeof, id chỉ có một kiểu duy nhất. Bước đó gọi là thu hẹp kiểu (narrowing), và chính nó làm cho union trở nên hữu dụng.
Chỉ được dùng thành viên chung
Trước khi thu hẹp, bạn chỉ dùng được những gì mọi thành viên của union đều hỗ trợ. toString() có ở cả chuỗi lẫn số nên dùng được; toUpperCase() chỉ có ở chuỗi:
index.ts(3,16): error TS2339: Property 'toUpperCase' does not exist on type 'string | number'.
Property 'toUpperCase' does not exist on type 'number'.
Dòng thứ hai chỉ ra thành viên thiếu thuộc tính đó. Quy tắc này cũng áp dụng theo chiều ngược lại: một giá trị string | number không thể truyền vào tham số kiểu string (TS2345), vì nó có thể là một số. Union chấp nhận nhiều giá trị hơn, và đổi lại, cho bạn làm ít việc hơn với chúng cho đến khi bạn kiểm tra.
Thu hẹp một union
Thu hẹp kiểu dùng các phép kiểm tra JavaScript thông thường. TypeScript theo dõi luồng điều khiển và loại bỏ dần các thành viên khi chúng bị loại trừ, nên sau phép kiểm tra cuối cùng chỉ còn lại một thành viên:
| Phép kiểm tra | Thu hẹp về | Phù hợp cho |
|---|---|---|
typeof x === "string" | kiểu nguyên thủy | string, number, boolean, bigint, symbol, undefined, function |
x === null, x === "a" | giá trị được so sánh | null, undefined, thành viên literal |
Array.isArray(x) | thành viên mảng | mảng |
x instanceof Date | lớp đó | instance của lớp |
"meow" in x | các thành viên có thuộc tính đó | kiểu đối tượng |
x.kind === "circle" | thành viên có nhãn đó | discriminated union |
isCat(x) (trả về x is Cat) | kiểu mà hàm khẳng định | mọi thứ, logic tùy chỉnh |
Danh sách đầy đủ các cách thu hẹp kiểu có ở trang type narrowing.
Union của literal type
Một union các giá trị literal là một tập hợp đóng các giá trị được phép. Đây là loại union phổ biến nhất trong code thực tế:
type Status = "idle" | "loading" | "success" | "error";
type Dice = 1 | 2 | 3 | 4 | 5 | 6;
type Toggle = "on" | "off" | boolean; // boolean is itself true | false
let current: Status = "idle";
current = "loading"; // fine
current = "finished"; // error TS2322: Type '"finished"' is not assignable to type 'Status'.
So sánh với một literal sẽ thu hẹp kiểu: sau if (current === "error"), nhánh else biết current là một trong ba giá trị còn lại. Union literal thay thế enum trong nhiều codebase; xem literal type để biết về as const và cách tạo một union như vậy từ một mảng.
Union của kiểu đối tượng
Khi các thành viên là kiểu đối tượng, những thuộc tính mà tất cả đều có sẽ dùng được ngay. Với phần còn lại, hãy kiểm tra thuộc tính có tồn tại không bằng in:
Với union gồm nhiều hình dạng đối tượng, cách gọn gàng hơn là một thuộc tính literal dùng chung như kind: "cat" / kind: "fish". Kiểm tra thuộc tính đó sẽ thu hẹp cả đối tượng, và một switch trên thuộc tính đó có thể được kiểm tra xem đã xét hết mọi trường hợp chưa. Mẫu này gọi là discriminated union.
Mảng và union
Vị trí đặt dấu ngoặc làm thay đổi hoàn toàn ý nghĩa:
| Kiểu | Nghĩa | Giá trị ví dụ |
|---|---|---|
(string | number)[] | một mảng mà mỗi phần tử là chuỗi hoặc số | [1, "two", 3] |
string[] | number[] | một mảng toàn chuỗi, hoặc một mảng toàn số | ["a", "b"] |
string | number[] | một chuỗi, hoặc một mảng số (| có độ ưu tiên thấp hơn []) | "text" |
Khi duyệt một (string | number)[], mỗi phần tử là một union và cần được thu hẹp, như trong callback của reduce ở trên. Các phương thức như map và filter cũng dùng được trên string[] | number[], với callback nhận vào string | number.
Union với null và undefined
Loại union phổ biến nhất là "một giá trị hoặc không có gì": string | null, User | undefined. Đó là thứ mà Array.prototype.find và Map.prototype.get trả về, và một thuộc tính tùy chọn name?: string được đọc là string | undefined. Cách xử lý chúng với ?., ?? và kiểm tra null có trang riêng: null và undefined.
Để loại bỏ thành viên khỏi một union có sẵn ở cấp độ kiểu, hãy dùng các tiện ích có sẵn: Exclude<"a" | "b" | "c", "a"> là "b" | "c", và NonNullable<string | null> là string.
Câu hỏi thường gặp
Union type trong TypeScript là gì?
Là một kiểu gồm nhiều lựa chọn nối với nhau bằng |. Một giá trị kiểu string | number có thể là chuỗi hoặc số. Trình biên dịch chỉ cho bạn dùng những gì mọi thành viên đều có, cho đến khi bạn thu hẹp giá trị về một thành viên bằng phép kiểm tra như typeof value === "string".
Vì sao TypeScript báo một thuộc tính không tồn tại trên union type?
Vì ít nhất một thành viên của union không có thuộc tính đó. Lỗi TS2339, ví dụ Property 'toUpperCase' does not exist on type 'string | number', nghĩa là giá trị có thể là một số, mà số thì không có toUpperCase. Hãy thu hẹp trước (typeof, in, Array.isArray, instanceof hoặc kiểm tra thuộc tính phân biệt), rồi mới dùng thuộc tính riêng của thành viên đó.
Làm sao khai báo một mảng chứa nhiều kiểu?
Đặt union trong ngoặc tròn: (string | number)[] hoặc Array<string | number>, khi đó mỗi phần tử có thể là một trong hai kiểu. string[] | number[] thì khác: cả mảng toàn là chuỗi hoặc toàn là số. Không có ngoặc, string | number[] nghĩa là một chuỗi hoặc một mảng số.
Union và intersection type khác nhau thế nào?
Một union A | B là giá trị thuộc một trong các kiểu, nên bạn chỉ dùng được những gì chúng có chung. Một intersection A & B là giá trị đồng thời thuộc cả hai kiểu, nên nó có mọi thành viên của cả hai. Với kiểu đối tượng, A | B chấp nhận nhiều giá trị hơn, còn A & B đòi hỏi nhiều thuộc tính hơn.
Làm sao kiểm tra một giá trị union thuộc kiểu nào?
Dùng một phép kiểm tra lúc chạy mà TypeScript hiểu được: typeof x === "string" cho kiểu nguyên thủy, Array.isArray(x) cho mảng, x instanceof Date cho lớp, "prop" in x cho hình dạng đối tượng, hoặc x.kind === "circle" khi các thành viên có chung một nhãn literal. Với logic tùy chỉnh, hãy viết một hàm type guard trả về x is T.