Menu

Intersection type trong TypeScript: kết hợp kiểu với &

Intersection type A & B mô tả một giá trị đồng thời là A và B, nên nó có mọi thành viên của cả hai. Tìm hiểu cách kết hợp kiểu object bằng &, vì sao thuộc tính xung đột trở thành never, vì sao intersection của các union chỉ giữ thành viên chung, và khi nào nên dùng extends.

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.

Intersection type, viết là A & B, mô tả một giá trị đồng thời là A và B. Với kiểu object, nghĩa là giá trị có mọi thuộc tính của cả hai. Đây là cách kết hợp các kiểu có sẵn mà không phải viết lại các thuộc tính.

Object thứ hai thiếu team, nên gây lỗi biên dịch TS2322, và dòng tiếp theo của thông báo ghi Property 'team' is missing ... but required in type 'Employee'. Một giá trị Staff có thể được truyền vào bất cứ chỗ nào cần một Person hay một Employee.

Kết hợp kiểu object

& dùng được với mọi tổ hợp type alias, interface và kiểu object viết trực tiếp, và với cả tham số kiểu generic. Trường hợp cuối là nơi nó khó bị thay thế: một hàm thêm thuộc tính vào bất kỳ object nào nó nhận được có thể nói điều đó một cách chính xác.

Nơi gọi giữ nguyên kiểu chính xác của thứ nó truyền vào (title, words) cộng với hai thuộc tính được thêm. Một interface ... extends không diễn đạt được "T là gì cũng được, cộng thêm mấy thuộc tính này", vì interface không thể extend một tham số kiểu.

Thuộc tính xung đột trở thành never

Khi cả hai vế khai báo cùng một thuộc tính, kiểu của thuộc tính là intersection của hai kiểu đó. Nếu hai kiểu không có giá trị chung nào, thuộc tính trở thành never, và trình biên dịch không nói gì cho tới khi bạn thử tạo một giá trị:

index.ts(7,22): error TS2322: Type 'string' is not assignable to type 'never'.

Lỗi trỏ vào object chứ không trỏ vào kiểu đã gây ra nó, nên những bug kiểu này rất mất thời gian để lần ra. Rê chuột lên r.id trong trình soạn thảo sẽ thấy kiểu của nó: never. Khi thuộc tính xung đột là một tag literal, như trong type Shape = { kind: "circle" } & { kind: "square" }, TypeScript còn đi xa hơn và rút gọn cả intersection thành never. Đọc một thuộc tính của giá trị như vậy sẽ thấy lời giải thích: Property 'kind' does not exist on type 'never'. The intersection 'Shape' was reduced to 'never' because property 'kind' has conflicting types in some constituents.

interface ... extends thì bắt cùng xung đột đó ngay tại khai báo, với lỗi TS2430. Đó là khác biệt thực tế chính giữa hai cách; trang so sánh interface và type đặt chúng cạnh nhau.

Phần giao tương thích thu hẹp thuộc tính

Nếu hai kiểu thuộc tính có phần giao nhau, kết quả là phần giao đó. Điều này hữu ích chứ không phải lỗi:

Nửa sau cho thấy & làm gì với union: nó giữ lại những thành viên mà cả hai vế cùng có. Coi kiểu là tập hợp các giá trị sẽ giúp đoán trước được điều này. A | B là hợp của hai tập, A & B là phần giao của chúng, và một phần giao rỗng là never.

Intersection và union

Hai cái tên này đến từ lý thuyết tập hợp, và chúng có vẻ ngược đời khi áp dụng cho thuộc tính của object:

A | B (union)A & B (intersection)
Một giá trị làmột A hoặc một Bmột A và một B
Tập giá trị được phéplớn hơnnhỏ hơn
Thuộc tính dùng đượcchỉ những thuộc tính có ở cả haimọi thuộc tính của từng bên
string với numberstring | numbernever
"a" | "b" với "b" | "c""a" | "b" | "c""b"

Intersection của các kiểu object có nhiều thuộc tính hơn chính vì nó cho phép ít giá trị hơn: chỉ những object có đủ mọi thứ.

Intersection và extends

type C = A & Binterface C extends A, B
Dùng vớimọi kiểu, kể cả union và tham số kiểukiểu object có các thành viên biết trước
Thuộc tính xung độtlặng lẽ thành neverlỗi TS2430 hoặc TS2320 tại khai báo
Kết quảmột intersection, được kiểm tra từng thành phầnmột kiểu có tên, phẳng, với các quan hệ được cache
Ghép nhiều kiểu lớncó thể làm chậm việc kiểm tra kiểuđược trang Performance trong wiki TypeScript khuyên dùng

Để gộp vài type alias kiểu object, & là cách viết quen thuộc và hoàn toàn ổn. Với một kiểu được ghép từ nhiều phần, hoặc một kiểu thuộc API công khai, extends báo lỗi sớm hơn và kiểm tra kiểu nhẹ hơn.

Intersection với kiểu nguyên thủy: branding

Giao một kiểu nguyên thủy với một kiểu object không cho ra never: string & { readonly __brand: "UserId" } là một chuỗi mang thêm một dấu hiệu chỉ tồn tại lúc biên dịch. Không chuỗi thật nào có thuộc tính đó, và đó chính là mục đích: chỉ code cố ý khẳng định brand mới tạo được giá trị như vậy, nên một string thông thường hay một OrderId không còn truyền được vào chỗ cần UserId. Kỹ thuật này có trang riêng, branded type.

Câu hỏi thường gặp

Intersection type trong TypeScript là gì?

Là kiểu được viết A & B, có giá trị phải thỏa mãn đồng thời cả A và B. Với kiểu object, nghĩa là giá trị có mọi thuộc tính của A và mọi thuộc tính của B. Đây là cách thông dụng để gộp hai type alias thành một.

Union và intersection khác nhau thế nào?

Union A | B nghĩa là "một trong hai": giá trị có thể là A hoặc B, và bạn chỉ dùng được phần chung của chúng cho tới khi thu hẹp kiểu. Intersection A & B nghĩa là "cả hai": giá trị có mọi thứ của cả hai bên. Với kiểu object, union chấp nhận nhiều giá trị hơn còn intersection có nhiều thuộc tính hơn.

Vì sao intersection type của tôi lại là never?

Vì không giá trị nào thỏa mãn được cả hai vế. string & number là never, còn { id: string } & { id: number } khiến id thành string & number, nên thuộc tính đó là never và không thể tạo object nào. Nếu hai kiểu object có cùng một tag literal nhưng khác giá trị (kind: "circle" và kind: "square"), cả intersection bị rút gọn thành never.

Nên dùng intersection hay extends?

Cả hai đều kết hợp kiểu object. interface X extends A, B báo thuộc tính xung đột ngay tại khai báo và được nhóm TypeScript khuyên dùng khi ghép những kiểu object lớn. & dùng được với mọi kiểu, kể cả union và tham số generic, những thứ extends không kết hợp được. Dùng & cho type alias và các hàm hỗ trợ generic, và extends khi xây dựng interface.

Làm sao gộp hai kiểu object trong TypeScript?

Viết type Merged = A & B. Với giá trị lúc chạy, spread cả hai object: const merged: A & B = { ...a, ...b }. Nếu A và B có chung một thuộc tính với kiểu khác nhau, thuộc tính đó thành never; hãy dùng Omit<A, keyof B> & B khi thuộc tính của object thứ hai cần thay thế thuộc tính của object thứ nhấ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