Type và Interface trong TypeScript

Một trong những câu hỏi được đặt thường xuyên nhất về TypeScript là liệu chúng ta nên sử dụng interface hay type. Câu trả lời cho câu hỏi này, giống như nhiều câu hỏi lập trình khác, là nó phụ thuộc vào tình hình cụ thể. Trong một số trường hợp, một cái có lợi thế rõ rệt hơn cái kia, nhưng trong nhiều trường hợp, chúng có thể thay thế cho nhau.
Type và Interface trong TypeScript
Type và Interface trong TypeScript

Chúng ta có hai lựa chọn để định nghĩa các loại trong TypeScript: TypeInterface. Một trong những câu hỏi được đặt thường xuyên nhất về TypeScript là liệu chúng ta nên sử dụng interface hay type.

Câu trả lời cho câu hỏi này, giống như nhiều câu hỏi lập trình khác, là nó phụ thuộc vào tình hình cụ thể. Trong một số trường hợp, một cái có lợi thế rõ rệt hơn cái kia, nhưng trong nhiều trường hợp, chúng có thể thay thế cho nhau.

Trong bài viết này, chúng ta sẽ thảo luận về những điểm khác biệt và tương đồng quan trọng giữa typeinterface và khám phá khi nào thì nên sử dụng từng cái.

Hãy bắt đầu từ những kiến thức cơ bản về typeinterface.

Types và type aliases

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

- String (Chuỗi)
- Boolean (Đúng sai)
- Number (Số)
- Array (Mảng)
- Tuple (Bộ)
- Enum (Kiểu liệt kê)
- Các loại nâng cao

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

Type aliases trong TypeScript có nghĩa là "một tên cho bất kỳ loại nào." Chúng cung cấp một cách để tạo ra tên mới cho các type hiện có. Type aliases 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 type hiện có.
Type aliases có thể được tạo bằng cách sử dụng từ khóa type, trỏ đến bất kỳ loại TypeScript hợp lệ nào, bao gồm cả các loại nguyên thủy.

type MyNumber = number

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

 

Trong ví dụ trên, chúng ta tạo ra hai type aliases: MyNumberUser. Chúng ta có thể sử dụng MyNumber làm cách viết tắt cho một loại số và sử dụng type alias User để đại diện cho định nghĩa type của một người dùng.

Bạn có thể tạo ra các type alias sau:

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

Hai type aliases ở trên đại diện cho các tên thay thế cho cùng một kiểu liên hợp: string | number. Mặc dù loại cơ bản là giống nhau, nhưng các tên khác nhau biểu thị những ý đồ khác nhau, làm cho mã nguồn dễ đọc hơn.

 

Interface

Trong TypeScript, một interface định nghĩa một contract - hợp đồng mà một đối tượng phải tuân theo. Dưới đây là một ví dụ:

interface Client {
  name: string
  address: string
}

Chúng ta có thể biểu thị định nghĩa contract cho Client bằng cách sử dụng type annotations như sau:

type Client = {
  name: string
  address: string
}

Cả hai cách trên đều định nghĩa cùng một hợp đồng cho Client. Tuy nhiên, interface thường được sử dụng khi định nghĩa contract vì chúng có một số tính năng và mục đích đặc biệt, như khả năng mở rộng hợp đồng và hỗ trợ cho tên của thuộc tính động, trong khi type annotations thường được sử dụng khi định nghĩa kiểu cụ thể.

 

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

Trong trường hợp trên, chúng ta có thể sử dụng cả typeinterface. Tuy nhiên, có một số tình huống trong đó sử dụng type thay vì interface sẽ tạo ra sự khác biệt.

 

Primitive types - Kiểu nguyên thủy

Kiểu nguyên thủy là các kiểu tích hợp sẵn trong TypeScript. Chúng bao gồm các loại number, string, boolean, nullundefined.
Chúng ta có thể định nghĩa một type alias cho một loại nguyên thủy như sau:

type Address = string

Chúng ta thường kết hợp kiểu nguyên thủy với union type để định nghĩa một type alias, để làm cho code dễ đọc hơn:

type NullOrUndefined = null | undefined

Tuy nhiên, chúng ta không thể sử dụng một interface để đặt tên thay thế cho một kiểu nguyên thủy. Interface chỉ có thể được sử dụng cho một loại object.
Do đó, khi chúng ta cần định nghĩa một type alias cho kiểu nguyên thủy, chúng ta bắt buộc phải sử dụng type.

 

Union type

Union type cho phép chúng ta mô tả các giá trị có thể thuộc về một trong một số loại và tạo ra sự kết hợp của các loại nguyên thủy, literal hoặc phức hợp:

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

Union type chỉ có thể được định nghĩa bằng cách sử dụng type. Không có đối tượng tương đương của union type trong một interface. Tuy nhiên, có thể tạo ra một union type mới từ hai interface như sau:

interface CarBattery {
  power: number
}

interface Engine {
  type: string
}

type HybridCar = Engine | CarBattery

 

Function types - kiểu hàm

Bằng cách sử dụng type alias, chúng ta cần chỉ định các tham số và loại trả về để định nghĩa kiểu function:

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

Chúng ta cũng có thể sử dụng một interface để đại diện cho kiểu function:

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

Cả typeinterface đều định nghĩa được các function tương tự, trừ sự khác biệt về cú pháp giữa interface sử dụng :=> khi sử dụng type. Trong trường hợp này, type được ưa chuộng vì nó ngắn gọn hơn và dễ đọc hơn.

Một lý do khác để sử dụng type khi định nghĩa function là khả năng mà interface thiếu. Khi function trở nên phức tạp hơn, chúng ta có thể tận dụng các tính năng nâng cao như conditional types, mapped types, v.v. Dưới đây là một ví dụ:

type Car = 'ICE' | 'EV'
type ChargeEV = (kws: number) => void
type FillPetrol = (type: string, liters: number) => void
type RefillHandler<A extends Car> = A extends 'ICE'
  ? FillPetrol
  : A extends 'EV'
  ? ChargeEV
  : never
const chargeTesla: RefillHandler<'EV'> = (power) => {
  // Triển khai để sạc xe điện (EV)
}
const refillToyota: RefillHandler<'ICE'> = (fuelType, amount) => {
  // Triển khai để đổ nhiên liệu cho xe động cơ đốt trong (ICE)
}

Ở đây, chúng ta định nghĩa một loại RefillHandler với conditional typeunion type. Nó cung cấp một function thống nhất cho các xử lý EVICE một cách an toàn về kiểu dữ liệu. Chúng ta không thể đạt được điều tương tự với interface vì nó không có cách làm tương đương của conditional typeunion type.

 

Declaration merging - Gộp khai báo

Declaration merging là một tính năng đặc biệt của các interface trong TypeScript. Với declaration merging, chúng ta có thể định nghĩa một interface nhiều lần, và trình biên dịch TypeScript sẽ tự động gộp các định nghĩa này thành một định nghĩa duy nhất của interface.

Trong ví dụ sau, hai khai báo Client được gộp lại thành một bởi trình biên dịch TypeScript, và khi sử dụng interface Client, chúng ta có hai thuộc tính:

interface Client {
  name: string
}

interface Client {
  age: number
}

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

Type aliases không thể được gộp lại theo cách tương tự. Nếu bạn cố gắng định nghĩa lại Client nhiều lần bằng type, như trong ví dụ trên, sẽ xảy ra lỗi:

// Duplicate identifier 'Client'

Khi sử dụng ở những nơi phù hợp, delaration merging có thể rất hữu ích. Một trường hợp sử dụng thông thường của delaration merging là mở rộng định nghĩa type của một thư viện của bên thứ ba để phù hợp với các yêu cầu cụ thể trong một dự án cụ thể.

Nếu bạn cần delaration merging, thì interface là lựa chọn tối ưu.

 

Extends và intersection

Một interface có thể mở rộng từ một hoặc nhiều interface khác. Bằng cách sử dụng từ khóa extends, một interface mới có thể kế thừa tất cả các properties và methods của một interface hiện có, đồng thời có thể thêm các properties/methods mới.

Ví dụ, chúng ta có thể tạo một interface VIPClient bằng cách mở rộng từ interface Client:

interface VIPClient extends Client {
  benefits: string[]
}

Để đạt được kết quả tương tự cho type, chúng ta cần sử dụng toán tử &:

type VIPClient = Client & { benefits: string[] }; // Client là một type

Bạn cũng có thể mở rộng một interface từ một type:

type Client = {
  name: string
}

interface VIPClient extends Client {
  benefits: string[]
}

Một ngoại lệ là các kiểu liên hợp (union type). Nếu bạn cố gắng mở rộng một interface từ một kiểu liên hợp, bạn sẽ gặp phải lỗi sau:

type Jobs = 'salary worker' | 'retired'

interface MoreJobs extends Jobs {
  description: string
}

// An interface can only be extended an object type or intersection of object type with statically known members.

Lỗi xảy ra vì loại liên hợp không được biết trước tĩnh. Định nghĩa của interface cần được biết trước tại thời điểm biên dịch.

Type aliases có thể mở rộng các interface bằng cách sử dụng toán tử &, như dưới đây:

interface Client {
  name: string
}

type VIPClient = Client & { benefits: string[] }

Tóm lại, cả interface và type alias có thể được mở rộng. Một interface có thể mở rộng từ một type alias biết trước tĩnh, trong khi một type alias có thể mở rộng từ một interface bằng cách sử dụng toán tử giao điểm.

 

Làm việc với kiểu tuple

Trong TypeScript, kiểu tuple cho phép chúng ta biểu thị một mảng với một số lượng phần tử cố định, trong đó mỗi phần tử có kiểu dữ liệu riêng của nó. Nó có thể hữu ích khi bạn cần làm việc với các mảng dữ liệu có cấu trúc cố định:

type Member = [name: string, role: string, age: number]

Interfaces không có hỗ trợ trực tiếp cho kiểu tuple. Mặc dù chúng ta có thể tạo một số cách thay thế như trong ví dụ dưới đây, nhưng nó không ngắn gọn hoặc dễ đọc như việc sử dụng kiểu tuple:

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

const peter: Member = ['Harry', 'Dev', 24]

const tom: Member = ['Tom', 30, 'Manager'] // Lỗi: Loại 'number' không gán được cho loại 'string'.

 

Các tính năng nâng cao

TypeScript cung cấp một loạt các tính năng nâng cao mà không thể tìm thấy trong các interface. Một số tính năng duy nhất trong TypeScript bao gồm:

- Type inferences - Kiểu suy luận: Có thể suy luận kiểu variable và function dựa trên cách chúng được sử dụng. Điều này giúp giảm mã và tăng khả năng đọc hiểu code.
- Conditional type - Các loại điều kiện: Cho phép chúng ta tạo ra biểu thức kiểu phức tạp với các hành vi có điều kiện phụ thuộc vào các type khác.
- Type guards - Kiểu kiểm tra: Được sử dụng để viết các dòng kiểm soát phức tạp dựa trên kiểu của một biến.
- Mapped type - Các loại được ánh xạ: Chuyển đổi một loại đối tượng hiện có thành một loại mới.
- Utility type - Các loại tiện ích: Một bộ các tiện ích có sẵn giúp thao tác với các type.

Hệ thống kiểu ấn tượng trong TypeScript là một trong những lý do chính mà nhiều nhà phát triển ưa thích sử dụng TypeScript.

 

Khi nào sử dụng type so với interface

Typeinterface tương tự nhau nhưng có sự khác biệt tinh tế, như được thể hiện ở phần trước.

Mặc dù hầu hết tất cả các tính năng của interface có sẵn trong type hoặc có tương đương, một ngoại lệ là declaration merging. Interfaces nên được sử dụng khi cần declaration merging, ví dụ như khi mở rộng một thư viện hiện có hoặc tạo một thư viện mới. Ngoài ra, nếu bạn ưa thích kiểu kế thừa OOP, việc sử dụng từ khóa extends với một interface thường được cho là dễ đọc hơn là sử dụng & với type.

Tuy nhiên, rất nhiều tính năng trong type khó hoặc không thể thực hiện được bằng cách sử dụng interface. Ví dụ, TypeScript cung cấp các tính năng phong phú như conditional types, generic types, type guards, advanced types và nhiều tính năng khác. Bạn có thể sử dụng chúng để xây dựng một hệ thống typing có ràng buộc tốt hơn cũng như an toàn hơn. Các interface không thể đạt được điều này.

Trong nhiều trường hợp, cả hai có thể được sử dụng xen kẽ tùy thuộc vào sở thích cá nhân. Tuy nhiên, chúng ta nên sử dụng type trong các trường hợp sử dụng sau đây:

- Để tạo một tên mới cho một kiểu nguyên thủy.
- Để định nghĩa một union type, tuple, function hoặc một kiểu phức tạp khác.
- Để overload - nạp chồng function.
- Để sử dụng conditional types, generic types, type guards, advanced types và nhiều tính năng khác.

So với interface, type biểu đạt phong phú hơn. Nhiều tính năng nâng cao không có sẵn trong các interface và các tính năng này sẽ tiếp tục phát triển khi TypeScript được nâng cấp theo thời gian.

Dưới đây là một ví dụ về tính năng nâng cao mà interface không thể làm được.

type Client = {
  name: string
  address: string
}

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}

type clientType = Getters<Client>

// tương đương với
// type clientType = {
//   getName: () => string
//   getAddress: () => string
// }

Bằng cách sử dụng mapped types, template literal types và toán tử keyof, chúng ta đã tạo ra một type tự động tạo các phương thức getter cho bất kỳ kiểu đối tượng nào.

Ngoài ra, nhiều nhà phát triển ưa thích sử dụng type vì chúng khớp tốt với mô hình lập trình hướng functional. Các biểu thức type phong phú giúp dễ dàng đạt được sự kết hợp chức năng, tính bất biến và các khả năng lập trình functional khác một cách an toàn về typing.

Kết luận

Trong bài viết này, chúng ta đã thảo luận về type và interface cùng với sự khác biệt giữa chúng. Mặc dù có một số tình huống mà một cái được ưa chuộng hơn cái kia, trong hầu hết các trường hợp, sự lựa chọn giữa chúng dựa vào sở thích cá nhân.

Mình ưa thích sử dụng type đơn giản vì hệ thống kiểu tuyệt vời. Ý kiến của bạn là gì? Bạn có thể chia sẻ ý kiến của mình trong phần bình luận bên dưới.

Comments

Bài viết nổi bật

Dạo gần đây đi đâu cũng nghe nói về microservices, người người nhà nhà rục rịch chuyển dịch hệ thống sang microservices. Trước khi đưa ra sự so sánh, mình sẽ khái quát một chút về Monolith Application và MicroServices một chút cho các bạn chưa biết nắm rõ hơn nhé.
PHP là ngôn ngữ được sử dụng rộng rãi nhất trên thế giới trong lập trình web. Nó cũng bị ghét nhất. Nhưng tại sao nhiều developer lại ghét nó đến vậy? Hôm nay chúng ta hãy cùng tìm hiểu lý do xem chúng có thuyết phục không nhé ^_^
Lúc trước mình hay sử dụng cách này trên laptop phụ của mình, giờ mua license luôn rồi. Hôm nay mình xin chia sẻ cho bạn nào cần nhé.
JWT Tokens là một cách thức lưu trữ thông tin xác thực hiệu quả, nhưng làm cách nào để chúng ta có thể giúp chúng an toàn hơn? Có 2 cách thường dùng để lưu trữ JWT Tokens là LocalStorage và Cookies. Bây giờ chúng ta sẽ bắt đầu "mổ xẻ" các ưu - nhược điểm của mỗi loại nhé.
Có khá nhiều bạn đã yêu cầu mình một bài viết về Repository Design Pattern. Vậy mục đích của nó là gì? Nó có thực sự cần thiết cho ứng dụng của bạn hay không? Những điểm mạnh, điểm yếu của nó là gì? Chúng ta cùng đi sâu tìm hiểu qua bài viết này nhé.

Mục lục

Related posts

Triển khai Saga Pattern trong microservices với NodeJS và Choreography-Based Saga
Mô hình Saga đưa ra một giải pháp có cấu trúc để giải quyết thách thức này. Nó cung cấp một phương pháp có hệ thống để quản lý transaction qua nhiều microservices. Điều này giải quyết những phức tạp của các transaction phân tán và hoàn toàn tương thích với các nguyên tắc của kiến trúc microservices, được đặc trưng bởi sự kết nối lỏng lẻo và khả năng triển khai độc lập của các service.
Một API cho phép giao tiếp hai chiều giữa các ứng dụng phần mềm thông qua các requests. Một Webhook là một API nhẹ, hỗ trợ chia sẻ dữ liệu một chiều được kích hoạt bởi các events.
Đây là các types cơ bản nhưng cũng phổ biến nhất trong Typescript. Một số types khác phức tạp hơn cũng được xây dựng dựa trên những types cơ bản này.
Trong thế giới lập trình, trách nhiệm lớn nhất của chúng ta không phải chỉ làm cho code chạy được, mà còn phải đảm bảo rằng các đoạn code mà chúng ta viết có thể dễ dàng kiểm tra và bảo trì trong một khoảng thời gian dài.
Phân trang - một thành phần không thể thiếu trong các ứng dụng có lượng dữ liệu lớn. Tuy nhiên, bạn hiểu được bao nhiêu về nó?
Javascript là một thành phần không thể thiếu đối với frontend developers. Tuy nhiên, ngay từ lúc ra đời, nó đã tồn tại khá nhiều vấn đề cần khắc phục. Đó là lý do tại sao từ 2015 (ES6) tới 2021 (ES12) ra đời nhằm giúp Javascript trở nên tốt hơn.
Dạo này mình làm việc với mấy bạn trên github, thấy hay xài mấy từ viết tắt mà mình không hiểu lắm. Thôi thì tổng hợp lại một list các từ viết tắt hay dùng trong github luôn cho ai cần :D
Dạo gần đây đi đâu cũng nghe nói về microservices, người người nhà nhà rục rịch chuyển dịch hệ thống sang microservices. Trước khi đưa ra sự so sánh, mình sẽ khái quát một chút về Monolith Application và MicroServices một chút cho các bạn chưa biết nắm rõ hơn nhé.
Cách bỏ qua câu lệnh --set-upstream quen thuộc cho các con lười
Mình sẽ giới thiệu 2 cách để xóa một property trong Javascript Object. Một cách sử dụng mutable - toán tử delete, một cách còn lại là immutable - tính năng Object Restructuring.
Đây là một khái niệm rất quan trọng trong Functional Programming. Ở đây mình sẽ cho ví dụ dựa trên Javascript, cụ thể là TypeScript, do đó mình hi vọng các bạn đã có một số kiến thức nhất định về JS trước. Điều này sẽ giúp bạn nắm bắt nội dung bài viết dễ dàng hơn.

Tin mới nhất

Triển khai Saga Pattern trong microservices với NodeJS và Choreography-Based Saga
Mô hình Saga đưa ra một giải pháp có cấu trúc để giải quyết thách thức này. Nó cung cấp một phương pháp có hệ thống để quản lý transaction qua nhiều microservices. Điều này giải quyết những phức tạp của các transaction phân tán và hoàn toàn tương thích với các nguyên tắc của kiến trúc microservices, được đặc trưng bởi sự kết nối lỏng lẻo và khả năng triển khai độc lập của các service.
Một API cho phép giao tiếp hai chiều giữa các ứng dụng phần mềm thông qua các requests. Một Webhook là một API nhẹ, hỗ trợ chia sẻ dữ liệu một chiều được kích hoạt bởi các events.
Một trong những câu hỏi được đặt thường xuyên nhất về TypeScript là liệu chúng ta nên sử dụng interface hay type. Câu trả lời cho câu hỏi này, giống như nhiều câu hỏi lập trình khác, là nó phụ thuộc vào tình hình cụ thể. Trong một số trường hợp, một cái có lợi thế rõ rệt hơn cái kia, nhưng trong nhiều trường hợp, chúng có thể thay thế cho nhau.
Trong phần này, chúng ta sẽ tìm hiểu một số khái niệm cơ bản nhất về AWS là gì và một số lợi ích khi sử dụng AWS.
Trở thành một software developer hiệu suất cao không phải là điều dễ dàng. Điều này đòi hỏi bạn phải có kỹ năng và kiến thức về lập trình, cũng như cách tiếp cận và giải quyết các vấn đề phức tạp. Tuy nhiên, nếu bạn có chút kiên nhẫn và sự nỗ lực, bạn hoàn toàn có thể trở thành một developer tài năng và thành công.
Đây là các types cơ bản nhưng cũng phổ biến nhất trong Typescript. Một số types khác phức tạp hơn cũng được xây dựng dựa trên những types cơ bản này.
Trong thế giới lập trình, trách nhiệm lớn nhất của chúng ta không phải chỉ làm cho code chạy được, mà còn phải đảm bảo rằng các đoạn code mà chúng ta viết có thể dễ dàng kiểm tra và bảo trì trong một khoảng thời gian dài.
Thông tin được định nghĩa dưới dạng dữ liệu, kiến thức về thông tin, và trí tuệ về tri thức.
Phân trang - một thành phần không thể thiếu trong các ứng dụng có lượng dữ liệu lớn. Tuy nhiên, bạn hiểu được bao nhiêu về nó?
Javascript là một thành phần không thể thiếu đối với frontend developers. Tuy nhiên, ngay từ lúc ra đời, nó đã tồn tại khá nhiều vấn đề cần khắc phục. Đó là lý do tại sao từ 2015 (ES6) tới 2021 (ES12) ra đời nhằm giúp Javascript trở nên tốt hơn.
Dạo này mình làm việc với mấy bạn trên github, thấy hay xài mấy từ viết tắt mà mình không hiểu lắm. Thôi thì tổng hợp lại một list các từ viết tắt hay dùng trong github luôn cho ai cần :D
Triển khai Saga Pattern trong microservices với NodeJS và Choreography-Based Saga
Mô hình Saga đưa ra một giải pháp có cấu trúc để giải quyết thách thức này. Nó cung cấp một phương pháp có hệ thống để quản lý transaction qua nhiều microservices. Điều này giải quyết những phức tạp của các transaction phân tán và hoàn toàn tương thích với các nguyên tắc của kiến trúc microservices, được đặc trưng bởi sự kết nối lỏng lẻo và khả năng triển khai độc lập của các service.
Một API cho phép giao tiếp hai chiều giữa các ứng dụng phần mềm thông qua các requests. Một Webhook là một API nhẹ, hỗ trợ chia sẻ dữ liệu một chiều được kích hoạt bởi các events.
Một trong những câu hỏi được đặt thường xuyên nhất về TypeScript là liệu chúng ta nên sử dụng interface hay type. Câu trả lời cho câu hỏi này, giống như nhiều câu hỏi lập trình khác, là nó phụ thuộc vào tình hình cụ thể. Trong một số trường hợp, một cái có lợi thế rõ rệt hơn cái kia, nhưng trong nhiều trường hợp, chúng có thể thay thế cho nhau.
Đây là các types cơ bản nhưng cũng phổ biến nhất trong Typescript. Một số types khác phức tạp hơn cũng được xây dựng dựa trên những types cơ bản này.