Khác biệt giữa Type vs Interface. Bạn đã thực sự phân biệt được chúng?

By Thái Nguyễn
Picture of the author
Published on
image alt attribute

Đây là một bài viết nâng cao kỹ năng TypeScript của bạn. Bài viết giúp tìm hiểu sâu về những đặc điểm nổi bật của Type và Interface!

Trong TypeScript, chúng ta thường có hai lựa chọn để định nghĩa các kiểu dữ liệu: type và interface. Vậy sự khác biệt giữa chúng là gì? Và trong trường hợp nào nên sử dụng type, trong khi đó trường hợp nào lại phù hợp hơn với interface?

Trong bài viết này, chúng ta sẽ cùng đi sâu tìm hiểu về Type và Interface để trả lời 2 câu hỏi trên nhé !!!

Let’s start ✌️

Table of content

Types và type aliases

type là một từ khóa mà chúng ta có thể sử dụng để định nghĩa hình dạng của dữ liệu. Các kiểu cơ bản trong TypeScript bao gồm:

  • Chuỗi (String)
  • Boolean
  • Số (Number)
  • Mảng (Array)
  • Bộ (Tuple)
  • Kiểu liệt kê (Enum)
  • Các kiểu nâng cao (Advanced types)

Mỗi kiểu có những đặc điểm và mục đích riêng, cho phép các nhà phát triển lựa chọn kiểu phù hợp cho từng trường hợp sử dụng cụ thể của họ.

Các Type alias trong TypeScript có nghĩa là “một tên cho bất kỳ type nào.” Chúng cung cấp một cách tạo tên mới cho các Type đã tồn tại. Các bí Type alias không định nghĩa các type mới; thay vào đó, chúng cung cấp một tên thay thế cho một kiểu đã có.

Các Type alias có thể được tạo ra bằng cách sử dụng từ khóa type, tham chiếu đến bất kỳ type TypeScript hợp lệ nào, bao gồm cả các kiểu nguyên thủy.

type MyNumber = number;
type User = {
  id: number;
  name: string;
  email: string;
}

Trong ví dụ trên chúng ta có 2 type alias: MyNumberUser. Chúng ta có thể sử dụng MyNumber như cách viết tắt cho 1 kiểu number và sử dụng User type alias để đại diện cho type định nghĩa của 1 user.

Khi so sánh Type vs Interface tức là chúng ta đang so sánh "Type alias vs Interface". VD chúng ta có thể tạo các alias như sau:

type ErrorCode = string | number;
type Answer = string | number;

Với 2 type alias ở trên, chúng đại diện cho cùng 1 union type: string | number nhưng với tên khác nhau. Mỗi type alias được đặt tên với ý định khác nhau, sử dụng trong ngữ cảnh khác nhau. Chúng giúp code dễ đọc hơn.

Interfaces trong TypeScript

Trong TypeScript, một Interface được sử dụng để mô tả hình dạng của 1 object. Dưới đây là một ví dụ:

interface Client { 
    name: string; 
    address: string;
}

Chúng ta có thể định nghĩa tương tự với type annotations:

type Client = {
    name: string;
    address: string;
};

Sự khác nhau giữa type và interface

Các trường hợp ở trên chúng ta đều có thể sử dụng type hoặc interface. Tuy nhiên trong một vài ngữ cảnh việc sử dụng type hay interface sẽ khác nhau.

Primitive types (type nguyên thủy)

Các Primitive types là các kiểu có sẵn trong TypeScripts. Chúng bao gồm: number, string, boolean, null, và undefined.

Chúng ta có thể định nghĩa một type alias cho một Primitive type:

type Address = string;

Chúng ta thường kết hợp primitive type với union type để định nghĩa một type alias để code dễ đọc hơn:

type NullOrUndefined = null | undefined;

interface không thể sử dụng để đại diện cho Primitive types như trên. Interface chỉ có thể được sử dụng cho 1 object type. Do đó, khi chúng ta cần định nghĩa một primitive type alias, chúng ta sử dụng type.

Union types

Union types cho phép chúng ta miêu tả các giá trị có thể là 1 hoặc nhiều kiểu giữ liệu và tạo kiểu liên kết các giá trị primative khác nhau hoặc các kiểu dữ liệu phức tạp:

type Transport = 'Bus' | 'Car' | 'Bike' | 'Walk';

Union type chỉ có thể được định nghĩa sử dụng type. Interface không thể sử dụng union như trên. Nhưng có thể tạo một union type từ 2 interface như sau:

interface CarBattery {
  power: number;
}
interface Engine {
  type: string;
}
type HybridCar = Engine | CarBattery;
Function types

Việc sử dụng type alias chúng ta cần khai báo parameters và return type để định nghĩa một function type

type AddFn =  (num1: number, num2:number) => number;

Chúng ta hoàn toàn có thể sử dụng interface để đại diện cho function type:

interface IAdd {
   (num1: number, num2:number): number;
}

type và interface giống nhau trong việc định nghĩa function types, ngoại trừ về syntax - interface sử dụng ":" vs "." => khi sử dụng type. Type được sử dụng nhiều hơn trong trường hợp này vì nó dễ đọc và ngắn gọn hơn.

Conditional type

Hãy cùng xem ví dụ sau:

type Car = 'ICE' | 'EV'; // union type
type ChargeEV = (kws: number)=> void; // function type
type FillPetrol = (type: string, liters: number) => void; // function type
type RefillHandler<A extends Car> = A extends 'ICE' ? FillPetrol : A extends 'EV' ? ChargeEV : never; // Conditional Type
const chargeTesla: RefillHandler<'EV'> = (power) => {
    // Implementation for charging electric cars (EV)
};
const refillToyota: RefillHandler<'ICE'> = (fuelType, amount) => {
    // Implementation for refilling internal combustion engine cars (ICE)
};

Trong ví dụ trên RefillHandler là một conditional type. Nó được xác định là function type dựa trên Car type A. Nếu AICE, nó sẽ là FillPetrol; Nếu A là EV, nó sẽ là ChargeEV. Nếu A không phải 1 trong 2 ICE, EV thì nó sẽ là type never.

Chúng ta sẽ không thể thực hiện tương tự đối với Interface nếu sử dụng conditional type và union type như trên.

Declaration merging

Declaration merging là một tính năng riêng biệt của interface. Chúng ta có thể khai báo 1 interface nhiều lần và Typescript compiler sẽ tự động gộp chúng lại thành 1 interface duy nhất.

Hãy cùng xem ví dụ sau:

interface Client { 
    name: string; 
}

interface Client {
    age: number;
}

const harry: Client = {
    name: 'Harry',
    age: 41
}

Type sẽ không được gộp như trên. Nếu chúng ta cố tình khai báo trùng tên khi sử dụng type, thì lỗi duplicate sẽ xuất hiện.

Một use case phổ biến sử dụng merging đó là extend third-party library type để đáp ứng được nhu cầu cụ thể của dự án.

Extends vs intersection

Một interface có thể extends một hoặc nhiều interface khác. Một interface có thể kế thừa tất cả các properties và method của 1 interface khác trong khi vẫn có thể thêm các properties mới bằng việc sử dụng extends keyword.

Ví dụ chúng ta có thể taoh một VIPClient interface bằng việc extend Client interface:

interface VIPClient extends Client {
    benefits: string[]
}

Để có thể làm tương tự đối với type, chúng ta sẽ sử dụng intersection operator:

type VIPClient = Client & {benefits: string[]}; // Client is a type

Chúng ta hoàn toàn có thể extend một interface từ một type:

type Client = {
    name: string;
};

interface VIPClient extends Client {
    benefits: string[]
}

Tuy nhiên đối với union type thì không. Nếu bạn cố gắng extend một interface từ một union type, bạn sẽ gặp lỗi như sau:

type Jobs = 'salary worker' | 'retired';

interface MoreJobs extends Jobs {
  description: string;
}
union-type-not-statically-known-error
Overload functions

Hãy cùng xem ví dụ sau khi extends interface:

interface Person {
  getPermission: () => string;
}

interface Staff extends Person {
   getPermission: () => string[];
}

Việc sử dụng chung property key là nguyên nhân dẫn đến lỗi:

Interface 'Staff' incorrectly extends interface 'Person'.
The types returned by 'getPermission()' are incompatible between these types.
Type 'string[]' is not assignable to type 'string'.ts(2430)

Nếu sử dụng type nó sẽ tự động merge tất cả các property thay vì throw errors:

type Person = {
  getPermission: (id: string) => string;
};

type Staff = Person & {
   getPermission: (id: string[]) => string[];
};

const AdminStaff: Staff = {
  getPermission: (id: string | string[]) =>{
    return (typeof id === 'string'?  'admin' : ['admin']) as string[] & string;
  }
}

Do đó nếu chúng ta cần overload function thì nên sử dụng type.

Tuple types

Trong Typescript, tuple type cho phép chúng ta biểu diễn một mảng với số lượng elements cố định. Mỗi element sẽ có kiểu data type của riêng nó. Nếu chúng ta cần 1 array có cấu trúc cố định thì nó sẽ rất hữu dụng:

type TeamMember = [name: string, role: string, age: number];

Interfaces không hỗ trợ trực tiếp Tuple types mặc dù chúng ta có thể tạo theo ví dụ dưới đây, tuy nhiên nó sẽ không dễ đọc và ngắn gọn như với type:

interface ITeamMember extends Array<string | number> 
{
 0: string; 1: string; 2: number 
}

const peter: ITeamMember = ['Harry', 'Dev', 24];
const Tom: ITeamMember = ['Tom', 30, 'Manager']; //Error: Type 'number' is not assignable to type 'string'.

Khi nào sử dụng types vs. interfaces

Trong nhiều trường hợp chúng ta hoàn toàn có thể sử dụng type thay thế cho interface và ngược lại.

Tuy nhiên chúng ta nên sử dụng Interface cho việc merging. ví dụ extend một thư việc có sẵn. Bên cạnh đó, nếu bạn thích style object-oriented inheritance, việc sử dụng keyword extends với interface thường dễ đọc hơn việc sử dụng với intersection với type.

Chúng ta nên sử dụng type aliases trong những trường hợp sau:

  • tạo 1 tên mới cho primitive type
  • định nghĩa union type, tuple type, function type, conditional types hoặc các kiểu type phức tạp
  • overload function

Conclusion

Trong bài viết này chúng ta đã nói về type aliases và interfaces và những điểm khác biệt của chúng. Một vài trường hợp thì sử dụng cái này một vài sử dụng cái kia tuy nhiên trong thực tế, việc lựa chọn chúng phụ thuộc chủ yếu vào sở thích cá nhân mỗi người.

Về phía mình thì mình thích sử dụng type hơn, còn bạn thì sao ? Hãy để lại bình luận ở phía dưới nhé !

Hẹn gặp lại trong những bài viết tiếp theo. Tạm biệt 🤗

alt