Khi mô tả hình dạng một object, interface và type làm cùng một việc, và giá trị của bên này gán được cho bên kia khi hình dạng khớp nhau. Khác biệt nằm ở các trường hợp biên: type đặt tên được cho những thứ interface không làm được (union, tuple, kiểu được tính toán), còn interface làm được vài việc type alias không làm được (merging, extends có kiểm tra).
TypeScript so sánh kiểu object theo cấu trúc, nên tên của khai báo không ảnh hưởng tới việc gán được hay không. Phần dưới đây liệt kê những chỗ mà lựa chọn này thực sự quan trọng.
Bảng so sánh
| Tính năng | interface | type |
|---|---|---|
| Hình dạng object | có | có |
| Tùy chọn, readonly, phương thức, index signature | có | có |
| Generic | có | có |
Union (A | B) | không | có |
| Tuple, kiểu nguyên thủy, kiểu hàm đứng riêng | không (kiểu hàm chỉ dưới dạng call signature) | có |
| Mapped type và conditional type | không | có |
| Mở rộng | extends, xung đột là lỗi | &, xung đột thành never |
| Declaration merging | có | không (trùng định danh) |
Gán được cho Record<string, T> | không | có, khi các thuộc tính phù hợp |
implements trong lớp | có | có, nếu là kiểu object |
| Định nghĩa đệ quy | có | có |
Những việc chỉ type làm được
Mọi thứ không phải một hình dạng object duy nhất đều cần type alias:
Không cái nào trong số này viết được bằng interface (hai cái cuối được giải thích trong mapped type và conditional type). Đây là lý do thực tế khiến codebase nào rồi cũng dùng type ở đâu đó, bất kể nó dùng gì cho hình dạng object.
Việc chỉ interface làm được: declaration merging
Hai khai báo interface cùng tên trong cùng phạm vi sẽ gộp làm một. Hai khai báo type cùng tên là lỗi TS2300, Duplicate identifier.
interface Settings {
theme: string;
}
interface Settings {
fontSize: number;
}
const s: Settings = { theme: "dark", fontSize: 14 }; // needs both
type Options = { a: number };
type Options = { b: number }; // error TS2300: Duplicate identifier 'Options'.
Merging là cách mở rộng kiểu của thư viện từ bên ngoài: thêm một thuộc tính vào Window toàn cục, vào Request của Express, hay vào kiểu theme của một thư viện. Nếu bạn phát hành kiểu mà người dùng có thể cần bổ sung, hãy dùng interface. Trong code ứng dụng của riêng bạn, một lần gộp ngoài ý muốn (hai tệp script khai báo cùng một tên interface toàn cục) xảy ra âm thầm, trừ khi cả hai khai báo cùng một thuộc tính với kiểu khác nhau; đây là một lý lẽ mà vài nhóm đưa ra để ưu tiên type.
extends và intersection
Interface mở rộng bằng extends, type alias bằng &. Phần lớn chúng cho cùng kết quả, nhưng xử lý thuộc tính xung đột theo cách khác nhau. extends báo xung đột ngay tại khai báo:
index.ts(7,11): error TS2430: Interface 'Broken' incorrectly extends interface 'Base'.
Types of property 'id' are incompatible.
Type 'number' is not assignable to type 'string'.
Intersection thì lặng lẽ chấp nhận cùng xung đột đó và biến thuộc tính thành never (một string đồng thời là number). Lỗi chỉ xuất hiện sau, khi bạn thử tạo một giá trị:
Lỗi trỏ vào object, cách xa khai báo đã gây ra nó. Khi xây dựng kiểu object từ các kiểu object khác, extends cho thông báo lỗi tốt hơn.
Index signature: một khác biệt dễ bỏ sót
Type alias cho kiểu object có một index signature ngầm định, nên có thể truyền vào chỗ cần Record<string, unknown>. Interface thì không. Đây là chủ ý: lời giải thích của nhóm TypeScript (issue #15300 trên repository TypeScript) là interface có thể được bổ sung bởi các khai báo sau đó, nên suy ra index signature cho nó kém an toàn hơn, và đổi quy tắc bây giờ sẽ làm hỏng quá nhiều code.
Lời gọi được đánh dấu @ts-expect-error vẫn chạy và in ra các field của Alan: quy tắc này chỉ áp dụng lúc biên dịch. Đây là nguyên nhân thường gặp của một lỗi khó hiểu khi truyền giá trị interface vào một hàm hỗ trợ ghi log, serialize hay truy vấn có kiểu Record<string, ...>. Hãy đổi riêng khai báo đó sang type, spread giá trị, hoặc khai báo kiểu tham số của hàm hỗ trợ bằng một interface hay một generic.
Hiệu năng và thông báo lỗi
Trang Performance trong wiki TypeScript (mục "Preferring Interfaces Over Intersections") gợi ý dùng interface Foo extends Bar, Baz { ... } thay cho type Foo = Bar & Baz & { ... } khi ghép các kiểu object. Lý do: interface là một kiểu object phẳng duy nhất và phát hiện được thuộc tính xung đột, quan hệ kiểu giữa các interface được cache (còn intersection xét như một tổng thể thì không), và kiểm tra một giá trị với intersection phải kiểm tra từng thành phần trước khi tới kiểu đã làm phẳng. Khác biệt này đáng kể trong codebase lớn có nhiều kiểu ghép; với vài hình dạng object bình thường, bạn sẽ không đo được.
Trang đó cũng lưu ý rằng interface hiển thị dễ đọc hơn. Interface được hiện bằng tên của nó khi rê chuột và trong thông báo lỗi, còn alias dạng intersection thường bị in ra bung thành từng phần, khiến các thông báo dài khó đọc hơn.
Nên dùng cái nào
Một quy tắc dùng được:
- Hình dạng object:
interface. Nó choextendscó kiểm tra, lỗi rõ ràng hơn khi ghép nhiều kiểu lớn, và cho phép người dùng thư viện bổ sung. Điều này khớp với gợi ý trong TypeScript handbook: "dùnginterfacecho tới khi bạn cần tới tính năng củatype". - Mọi thứ còn lại:
type. Union, tuple, kiểu hàm, kiểu literal, và mọi thứ được xây bằng mapped type, conditional type hay template literal type. - Ngoại lệ: dùng
typecho một hình dạng object phải khớp với tham số kiểuRecord<string, ...>, hoặc khi bạn cố ý muốn ngăn merging.
Dùng type cho mọi thứ cũng là một lựa chọn nhất quán, và nhiều codebase làm vậy. Trộn cả hai một cách ngẫu nhiên là lựa chọn duy nhất nên tránh: nó khiến người đọc phải tự hỏi liệu có một khác biệt nào được chủ ý hay không.
Câu hỏi thường gặp
type và interface trong TypeScript khác nhau thế nào?
Cả hai đều mô tả hình dạng object và có thể thay thế nhau cho việc đó. type còn đặt tên được cho union, tuple, kiểu nguyên thủy, mapped type hay conditional type, điều mà interface không làm được. interface hỗ trợ declaration merging và extends, vốn kiểm tra thuộc tính xung đột. Kiểu object viết bằng type còn gán được cho kiểu có index signature như Record<string, unknown>, còn interface thì không.
Nên dùng type hay interface?
Quy tắc gợi ý trong TypeScript handbook là: dùng interface cho tới khi bạn cần một tính năng chỉ type có. Trên thực tế, nghĩa là interface cho hình dạng object và type cho union, tuple, kiểu hàm và kiểu được tính toán. Những nhóm dùng type cho mọi thứ cũng vẫn ổn; điều quan trọng là một quy tắc nhất quán.
interface có nhanh hơn type trong TypeScript không?
Khi ghép các kiểu object, có trong một số trường hợp. Hướng dẫn hiệu năng của nhóm TypeScript khuyên dùng interface ... extends thay cho những intersection lớn (A & B & { ... }), vì quan hệ giữa các interface được cache và một interface là một kiểu phẳng duy nhất. Với một hình dạng object đơn giản thì không có khác biệt đáng kể.
Một lớp có implement được type alias không?
Được, nếu alias đó là kiểu object (hoặc intersection của các kiểu object): class Point implements PointType { ... } hoạt động. Một lớp không thể implement một union type; đó là lỗi TS2422.
Interface có extend được type alias không?
Được, miễn alias đó là kiểu object: type Base = { id: string }; interface User extends Base { name: string } là hợp lệ. Nó không thể extend một alias dạng union. Theo chiều ngược lại, type alias có thể xây dựng trên một interface bằng &.