Menu

Union type trong TypeScript: toán tử | và ví dụ

Một union type như string | number nghĩa là giá trị có thể là một trong nhiều kiểu. Tìm hiểu những gì bạn làm được với một union (chỉ những gì mọi thành viên đều hỗ trợ), cách thu hẹp nó, union của literal và kiểu đối tượng, cùng sự khác nhau giữa (A | B)[] và A[] | B[].

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.

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 traThu hẹp vềPhù hợp cho
typeof x === "string"kiểu nguyên thủystring, number, boolean, bigint, symbol, undefined, function
x === null, x === "a"giá trị được so sánhnull, undefined, thành viên literal
Array.isArray(x)thành viên mảngmảng
x instanceof Datelớp đóinstance của lớp
"meow" in xcá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 địnhmọ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ểuNghĩaGiá 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.

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