Nhảy tới nội dung

Sự khác biệt giữa interface và type

Bằng cách sử dụng type alias, bạn có thể định nghĩa tương tự như interface.

ts
interface Animal {
name: string;
bark(): string;
}
type Animal = {
name: string;
bark(): string;
};
ts
interface Animal {
name: string;
bark(): string;
}
type Animal = {
name: string;
bark(): string;
};

Chương này sẽ giải thích chi tiết sự khác biệt giữa interface và type alias.

Sự khác biệt giữa interface và type alias

Nội dungInterfaceType alias
Kế thừaCó thểKhông thể. Tuy nhiên có thể biểu diễn bằng intersection type
Override khi kế thừaOverride hoặc errorIntersection type được tính cho mỗi field
Khai báo cùng tênĐịnh nghĩa được mergeError
Mapped TypesKhông sử dụng đượcSử dụng được

Kế thừa

Interface có thể kế thừa interface hoặc type alias.

ts
interface Animal {
name: string;
}
type Creature = {
dna: string;
};
interface Dog extends Animal, Creature {
dogType: string;
}
ts
interface Animal {
name: string;
}
type Creature = {
dna: string;
};
interface Dog extends Animal, Creature {
dogType: string;
}

Mặt khác, type alias không thể kế thừa. Thay vào đó, bằng cách sử dụng intersection type (&), có thể thực hiện điều tương tự như kế thừa.

ts
type Animal = {
name: string;
};
type Creature = {
dna: string;
};
type Dog = Animal &
Creature & {
dogType: string;
};
ts
type Animal = {
name: string;
};
type Creature = {
dna: string;
};
type Dog = Animal &
Creature & {
dogType: string;
};

Override property

Khi override property trong quá trình kế thừa interface, type của property từ nguồn kế thừa sẽ bị ghi đè.

ts
// OK
interface Animal {
name: any;
price: {
yen: number;
};
legCount: number;
}
 
interface Dog extends Animal {
name: string;
price: {
yen: number;
dollar: number;
};
}
 
// Định nghĩa cuối cùng của Dog
interface Dog {
name: string;
price: {
yen: number;
dollar: number;
};
legCount: number;
}
ts
// OK
interface Animal {
name: any;
price: {
yen: number;
};
legCount: number;
}
 
interface Dog extends Animal {
name: string;
price: {
yen: number;
dollar: number;
};
}
 
// Định nghĩa cuối cùng của Dog
interface Dog {
name: string;
price: {
yen: number;
dollar: number;
};
legCount: number;
}

Tuy nhiên, để override, phải có thể gán được vào type gốc. Ví dụ sau là trường hợp cố gắng override field có type number bằng type string.

ts
interface A {
numberField: number;
price: {
yen: number;
dollar: number;
};
}
 
interface B extends A {
Interface 'B' incorrectly extends interface 'A'. Types of property 'numberField' are incompatible. Type 'string' is not assignable to type 'number'.2430Interface 'B' incorrectly extends interface 'A'. Types of property 'numberField' are incompatible. Type 'string' is not assignable to type 'number'.
numberField: string;
price: {
yen: number;
euro: number;
};
}
ts
interface A {
numberField: number;
price: {
yen: number;
dollar: number;
};
}
 
interface B extends A {
Interface 'B' incorrectly extends interface 'A'. Types of property 'numberField' are incompatible. Type 'string' is not assignable to type 'number'.2430Interface 'B' incorrectly extends interface 'A'. Types of property 'numberField' are incompatible. Type 'string' is not assignable to type 'number'.
numberField: string;
price: {
yen: number;
euro: number;
};
}

Mặt khác, trong trường hợp type alias, không phải là ghi đè mà intersection type của type của field được tính toán. Ngoài ra, ngay cả khi có mâu thuẫn trong intersection type và không thể tính toán, cũng không xảy ra compile error.

ts
type Animal = {
name: number;
price: {
yen: number;
dollar: number;
};
};
 
type Dog = Animal & {
name: string;
price: {
yen: number;
euro: number;
};
};
 
// Định nghĩa cuối cùng của Dog
type Dog = {
name: never; // Khi không thể tạo intersection type, thành type never thay vì compile error
price: {
yen: number;
dollar: number;
euro: number;
};
};
ts
type Animal = {
name: number;
price: {
yen: number;
dollar: number;
};
};
 
type Dog = Animal & {
name: string;
price: {
yen: number;
euro: number;
};
};
 
// Định nghĩa cuối cùng của Dog
type Dog = {
name: never; // Khi không thể tạo intersection type, thành type never thay vì compile error
price: {
yen: number;
dollar: number;
euro: number;
};
};

Khai báo cùng tên

Type alias không thể định nghĩa nhiều type cùng tên, sẽ xảy ra compile error.

ts
type SameNameTypeWillError = {
Duplicate identifier 'SameNameTypeWillError'.2300Duplicate identifier 'SameNameTypeWillError'.
message: string;
};
type SameNameTypeWillError = {
Duplicate identifier 'SameNameTypeWillError'.2300Duplicate identifier 'SameNameTypeWillError'.
detail: string;
};
ts
type SameNameTypeWillError = {
Duplicate identifier 'SameNameTypeWillError'.2300Duplicate identifier 'SameNameTypeWillError'.
message: string;
};
type SameNameTypeWillError = {
Duplicate identifier 'SameNameTypeWillError'.2300Duplicate identifier 'SameNameTypeWillError'.
detail: string;
};

Mặt khác, trong trường hợp interface, có thể định nghĩa interface cùng tên, và sẽ trở thành interface tổng hợp tất cả các định nghĩa cùng tên.
Tuy nhiên, nếu field cùng tên nhưng định nghĩa type khác nhau, sẽ xảy ra compile error.

ts
interface SameNameInterfaceIsAllowed {
myField: string;
sameNameSameTypeIsAllowed: number;
sameNameDifferentTypeIsNotAllowed: string;
}
 
interface SameNameInterfaceIsAllowed {
newField: string;
sameNameSameTypeIsAllowed: number;
}
 
interface SameNameInterfaceIsAllowed {
sameNameDifferentTypeIsNotAllowed: number;
Subsequent property declarations must have the same type. Property 'sameNameDifferentTypeIsNotAllowed' must be of type 'string', but here has type 'number'.2717Subsequent property declarations must have the same type. Property 'sameNameDifferentTypeIsNotAllowed' must be of type 'string', but here has type 'number'.
}
ts
interface SameNameInterfaceIsAllowed {
myField: string;
sameNameSameTypeIsAllowed: number;
sameNameDifferentTypeIsNotAllowed: string;
}
 
interface SameNameInterfaceIsAllowed {
newField: string;
sameNameSameTypeIsAllowed: number;
}
 
interface SameNameInterfaceIsAllowed {
sameNameDifferentTypeIsNotAllowed: number;
Subsequent property declarations must have the same type. Property 'sameNameDifferentTypeIsNotAllowed' must be of type 'string', but here has type 'number'.2717Subsequent property declarations must have the same type. Property 'sameNameDifferentTypeIsNotAllowed' must be of type 'string', but here has type 'number'.
}

Mapped Types

Mapped Types sẽ được giải thích chi tiết ở trang khác, ở đây chỉ giải thích có thể sử dụng với type alias hay interface.

📄️ Mapped Types

Với index type, bạn có thể tự do thiết lập bất kỳ key nào khi gán giá trị, nhưng khi truy cập phải kiểm tra undefined mỗi lần. Nếu format input đã được xác định rõ ràng, bạn có thể cân nhắc sử dụng Mapped Types.

Mapped Types là cơ chế cho phép chỉ định key của type một cách động, và chỉ có thể sử dụng với type alias.
Ví dụ sau tạo type mới với danh sách union type làm key.

typescript
type SystemSupportLanguage = "en" | "fr" | "it" | "es";
type Butterfly = {
[key in SystemSupportLanguage]: string;
};
typescript
type SystemSupportLanguage = "en" | "fr" | "it" | "es";
type Butterfly = {
[key in SystemSupportLanguage]: string;
};

Nếu sử dụng Mapped Types với interface sẽ xảy ra error.

typescript
type SystemSupportLanguage = "en" | "fr" | "it" | "es";
 
interface Butterfly {
[key in SystemSupportLanguage]: string;
A mapped type may not declare properties or methods.7061A mapped type may not declare properties or methods.
}
typescript
type SystemSupportLanguage = "en" | "fr" | "it" | "es";
 
interface Butterfly {
[key in SystemSupportLanguage]: string;
A mapped type may not declare properties or methods.7061A mapped type may not declare properties or methods.
}

Phân biệt sử dụng interface và type alias

Vậy khi thực tế định nghĩa type, nên sử dụng interface hay type alias? Rất tiếc, không có câu trả lời chính xác rõ ràng cho vấn đề này.

Cả interface và type alias đều có thể định nghĩa type, nhưng có những điểm khác nhau về khả năng mở rộng và khả năng sử dụng Mapped Types, vì vậy hãy cân nhắc những ưu nhược điểm này để quyết định quy tắc trong dự án và tuân thủ nó.

Làm ví dụ tham khảo, trong mục Type Aliases vs Interfaces của style guide TypeScript mà Google công khai, khuyến nghị sử dụng type alias khi định nghĩa type cho primitive value, union type hoặc tuple, và sử dụng interface khi định nghĩa type cho object.

Nếu việc phân biệt sử dụng interface và type alias gây khó khăn và làm chậm tốc độ phát triển, cũng có cách nghĩ là thống nhất viết bằng type alias.

Ví dụ sử dụng interface

Khi tạo library mà cấu trúc của type được định nghĩa phụ thuộc vào phía application, việc sử dụng interface là phù hợp.

Type định nghĩa của process.env trong Node.js được implement trong @types/node/process.d.ts như sau.

ts
declare module "process" {
global {
namespace NodeJS {
interface ProcessEnv extends Dict<string> {
TZ?: string;
}
}
}
}
ts
declare module "process" {
global {
namespace NodeJS {
interface ProcessEnv extends Dict<string> {
TZ?: string;
}
}
}
}

Vì được định nghĩa bằng interface, phía sử dụng package có thể tự do mở rộng type.

Nếu ProcessEnv được định nghĩa bằng type alias, sẽ không thể mở rộng type và trở nên rất khó phát triển. Như vậy, khi nhiều user không xác định tham chiếu type, hãy định nghĩa type bằng interface để cân nhắc khả năng mở rộng.

ts
// src/types/global.d.ts
declare module "process" {
global {
namespace NodeJS {
interface ProcessEnv {
NODE_ENV: "development" | "production";
}
}
}
}
ts
// src/types/global.d.ts
declare module "process" {
global {
namespace NodeJS {
interface ProcessEnv {
NODE_ENV: "development" | "production";
}
}
}
}

Thông tin liên quan

📄️ Interface

Interface là kiểu định nghĩa field và method mà class cần implement. Class implement interface để có thể kiểm tra xem có tuân theo tên method và kiểu tham số mà interface yêu cầu hay không.

📄️ Type alias

TypeScript cho phép đặt tên cho kiểu. Kiểu có tên được gọi là type alias.