Interface đặt tên cho hình dạng của một object: những thuộc tính nó phải có và kiểu của từng thuộc tính. Sau khi khai báo, bạn dùng tên đó như một kiểu, và trình biên dịch kiểm tra mọi object bạn truyền vào, trả về hay gán theo nó.
Lời gọi cuối cùng là lỗi biên dịch TS2741. Interface bị xóa khi code được biên dịch: JavaScript đầu ra không còn dấu vết gì của User, và không có gì kiểm tra hình dạng lúc chạy.
Khai báo interface
Cú pháp gồm từ khóa interface, một cái tên (theo quy ước là PascalCase) và phần thân liệt kê các thành viên. Các thành viên có thể ngăn cách bằng dấu chấm phẩy, dấu phẩy hoặc chỉ cần xuống dòng; dấu chấm phẩy là cách viết phổ biến.
interface Product {
sku: string; // required property
price: number;
tags: string[]; // array property
dimensions: { // nested object type
width: number;
height: number;
};
discount?: number; // optional property
readonly createdAt: Date; // cannot be reassigned
label(): string; // method
}
Interface là một kiểu, không phải giá trị. Nó không thể được khởi tạo bằng new, không có giá trị mặc định, và obj instanceof Product là lỗi TS2693 ('Product' only refers to a type, but is being used as a value here). Để kiểm tra hình dạng lúc chạy, hãy viết một type guard.
Structural typing và kiểm tra thuộc tính thừa
TypeScript so sánh hình dạng, không so sánh tên. Bất kỳ object nào có đủ các thuộc tính bắt buộc đều khớp với interface, dù có được khai báo bằng interface đó hay không. Thuộc tính thừa cũng không sao, trừ một trường hợp: một object literal viết thẳng vào chỗ cần interface sẽ bị kiểm tra thuộc tính thừa, vì một khóa lạ ở đó gần như luôn là lỗi gõ.
Lỗi đó (TS2353) chính là thứ bắt được { id: 1, name: "a", emial: "x" } cho một User: trình biên dịch còn gợi ý Did you mean to write 'email'? (TS2561).
Thuộc tính tùy chọn và readonly
Dấu ? sau tên làm cho thuộc tính thành tùy chọn: object có thể bỏ qua nó, và khi đọc sẽ được kiểu T | undefined. readonly cấm gán lại thuộc tính sau khi object đã được tạo.
Ở đây lộ ra hai giới hạn. Thứ nhất, readonly chỉ có tác dụng lúc biên dịch: hai dòng được đánh dấu @ts-expect-error vẫn chạy khi bạn nhấn Chạy, và chúng chạy thành công, còn mấy dòng cuối đổi apiUrl qua một tham chiếu có kiểu không có readonly. Thứ hai, nó chỉ áp dụng ở bề mặt: readonly hosts: string[] sẽ chặn gán lại hosts nhưng vẫn cho phép hosts.push(...), vì vậy bản thân mảng được khai báo kiểu readonly string[]. Nó ghi lại và thực thi ý định trong code có kiểu; nó không đóng băng thứ gì. Utility type Readonly<T> biến mọi thuộc tính của một interface có sẵn thành readonly cùng lúc.
Phương thức và thuộc tính kiểu hàm
Một phương thức có thể viết dưới dạng method signature, name(params): ReturnType, hoặc dưới dạng một thuộc tính chứa hàm, name: (params) => ReturnType. Nơi gọi dùng cả hai theo cùng một cách.
Khác biệt khá tinh tế: với strictFunctionTypes (nằm trong strict), tham số của thuộc tính kiểu hàm được kiểm tra chặt, còn method signature được kiểm tra lỏng hơn (bivariant), nên dạng thuộc tính bắt được thêm vài lỗi. Cú pháp phương thức ngắn hơn và phổ biến hơn; dùng cách nào cũng được.
Interface cũng có thể mô tả thứ gọi được hoặc khởi tạo được, bằng call signature hoặc construct signature:
interface Formatter {
(value: number): string; // call signature: the object is a function
locale: string; // and it also has a property
}
interface PointConstructor {
new (x: number, y: number): { x: number; y: number }; // construct signature
}
Index signature
Khi chưa biết trước tên các thuộc tính, một index signature mô tả tất cả chúng cùng lúc: [key: string]: T nghĩa là "khóa chuỗi bất kỳ, mỗi khóa chứa một T".
Mấy dòng cuối cho thấy cái bẫy: đọc một khóa không tồn tại vẫn có kiểu number, không phải number | undefined. Tùy chọn trình biên dịch noUncheckedIndexedAccess thêm | undefined vào mọi lần đọc như vậy.
Thuộc tính có tên có thể đứng cạnh một index signature, nhưng phải khớp với nó. interface Dict { [key: string]: number; name: string } là lỗi TS2411, Property 'name' of type 'string' is not assignable to 'string' index type 'number'. Hãy mở rộng kiểu chỉ số ([key: string]: number | string) hoặc tách phần động ra một thuộc tính riêng. Với các bảng khóa/giá trị đơn giản, Record<string, number> nói cùng điều đó chỉ trong một dòng.
Kế thừa interface
extends tạo một interface mới từ một hoặc nhiều interface có sẵn. Interface con có mọi thành viên của interface cha cộng với thành viên của riêng nó:
interface Animal {
name: string;
}
interface Pet extends Animal {
owner: string;
}
interface Trained {
commands: string[];
}
interface ServiceDog extends Pet, Trained {
certifiedUntil: Date;
}
// ServiceDog requires: name, owner, commands, certifiedUntil
Interface con chỉ được khai báo lại thuộc tính của interface cha với kiểu tương thích (hẹp hơn), chẳng hạn kind: "dog" khi interface cha ghi kind: string. Các quy tắc, và cách mở rộng type alias, có ở trang extends.
Implement interface trong lớp
class X implements Shape yêu cầu trình biên dịch kiểm tra rằng lớp có mọi thứ interface đòi hỏi. Thiếu một thành viên là lỗi ngay tại khai báo lớp:
index.ts(7,7): error TS2420: Class 'Circle' incorrectly implements interface 'Shape'.
Property 'area' is missing in type 'Circle' but required in type 'Shape'.
Khi đã thêm area(), nhiều lớp và cả một object thuần đều có thể được dùng như một Shape:
implements chỉ là một phép kiểm tra. Nó không thêm thành viên vào lớp, và cũng không khai báo kiểu tham số phương thức của lớp thay bạn: greet(name) {} trong một lớp implement greet(name: string): string vẫn là lỗi TS7006, Parameter 'name' implicitly has an 'any' type. Một lớp có thể implement nhiều interface: class A implements B, C.
Declaration merging
Khai báo hai interface cùng tên trong cùng một phạm vi sẽ gộp cả hai thành một. Đây là điều type alias không làm được (một type thứ hai cùng tên là lỗi trùng định danh).
interface Settings {
theme: string;
}
interface Settings {
fontSize: number;
}
// Settings now requires both properties
const s: Settings = { theme: "dark", fontSize: 14 };
Trong code ứng dụng, đây hiếm khi là điều bạn muốn, và một lần gộp ngoài ý muốn có thể gây khó hiểu. Công dụng thật sự của nó là thêm thành viên vào những kiểu bạn không sở hữu: tùy chọn của một thư viện, hoặc một biến toàn cục như Window. Từ bên trong một module, hãy bọc khai báo trong declare global:
declare global {
interface Window {
analytics: { track(event: string): void };
}
}
export {};
Sau đó, window.analytics.track("signup") được kiểm tra kiểu ở mọi nơi trong dự án. Các package định nghĩa kiểu dựa vào đúng cơ chế này; xem tệp khai báo.
Giá trị mặc định cho thuộc tính của interface
Interface không thể chứa giá trị mặc định, vì nó mô tả kiểu và bị xóa lúc chạy. size?: "sm" | "md" = "md" là lỗi TS1246, An interface property cannot have an initializer. Hãy để thuộc tính là tùy chọn và điền giá trị mặc định ở nơi dùng object:
Giá trị mặc định khi destructuring là lựa chọn an toàn hơn: chúng được áp dụng mỗi khi giá trị là undefined, kể cả size: undefined tường minh. Bản dùng spread chép đè undefined tường minh đó lên giá trị mặc định, và kết quả vẫn được gán kiểu như thể size luôn có giá trị. Nếu bạn cần kiểu đảm bảo điều đó, hãy bật exactOptionalPropertyTypes, khi đó size: undefined là lỗi biên dịch với một size?: ... tùy chọn. Lựa chọn còn lại là một lớp có field được khởi tạo, khi object cũng cần có hành vi.
Interface generic
Interface có thể nhận tham số kiểu, nhờ đó một khai báo dùng được cho nhiều kiểu dữ liệu:
interface ApiResponse<T> {
ok: boolean;
data: T;
error?: string;
}
interface Page<T> {
items: T[];
nextCursor?: string;
}
interface User {
id: number;
name: string;
}
const res: ApiResponse<Page<User>> = {
ok: true,
data: { items: [{ id: 1, name: "Ada" }], nextCursor: "abc" },
};
ApiResponse<Page<User>> đọc là "một response có data là một trang danh sách user". Thư viện chuẩn có rất nhiều kiểu như vậy: Array<T>, Promise<T>, Map<K, V> đều là interface generic.
Interface và type alias
Một type alias có thể mô tả cùng hình dạng object, và với kiểu object thuần, hai cách này thay thế được cho nhau. Chỉ interface mới merge được; chỉ type alias mới đặt tên được cho union, tuple, mapped type hay conditional type. Quy tắc kinh nghiệm trong TypeScript handbook là dùng interface cho tới khi bạn cần một tính năng chỉ type có. Trang so sánh interface và type có đầy đủ chi tiết, kể cả khác biệt về Record<string, ...> khiến phần lớn mọi người bất ngờ.
Câu hỏi thường gặp
Interface trong TypeScript là gì?
Interface là một mô tả có tên cho hình dạng của object: tên các thuộc tính, kiểu của chúng, thuộc tính nào là tùy chọn hay readonly, và các phương thức. Trình biên dịch kiểm tra để những giá trị được dùng như interface đó có đúng hình dạng ấy. Interface chỉ tồn tại lúc biên dịch; chúng không sinh ra JavaScript nào.
Làm sao đặt giá trị mặc định trong một interface TypeScript?
Không làm được: interface mô tả kiểu chứ không phải giá trị, nên size: "md" = ... không phải cú pháp hợp lệ. Hãy đánh dấu thuộc tính là tùy chọn (size?: "sm" | "md") và gán mặc định ở nơi dùng object, thường bằng giá trị mặc định khi destructuring trong tham số hàm: function render({ size = "md" }: Options). Spread một object mặc định ({ ...DEFAULTS, ...options }) cũng được, nhưng một undefined tường minh trong options sẽ ghi đè giá trị mặc định.
Làm sao kiểm tra lúc chạy xem một object có khớp interface không?
Không có cách nào có sẵn, vì interface bị xóa khi biên dịch: obj instanceof User là lỗi TS2693 ('User' only refers to a type, but is being used as a value here). Hãy viết một hàm type guard kiểm tra các thuộc tính, function isUser(x: unknown): x is User { ... }, hoặc xác thực bằng một thư viện schema.
Một interface có kế thừa được nhiều interface không?
Được. Liệt kê chúng sau extends, cách nhau bằng dấu phẩy: interface ServiceDog extends Pet, Trained { ... }. Interface mới có mọi thành viên của từng interface cha cộng với thành viên của riêng nó. Nếu hai interface cha khai báo cùng một thuộc tính với kiểu không tương thích, khai báo đó là lỗi.
Interface và lớp trong TypeScript khác nhau thế nào?
Lớp tồn tại lúc chạy: nó có hàm khởi tạo, phần cài đặt phương thức, và new tạo object từ nó. Interface chỉ mô tả hình dạng cho trình biên dịch và bị xóa khỏi JavaScript đầu ra. Một lớp có thể khai báo implements SomeInterface để trình biên dịch kiểm tra xem nó có khớp không, và bất kỳ object thuần nào có đúng hình dạng cũng khớp với interface đó.