Interface TypeScript là một trong những tính năng của TypeScript giúp định nghĩa cấu trúc của dữ liệu hoặc hình dạng của một đối tượng, đem lại sự tối ưu hóa mã nguồn hiệu quả. Đây cũng là tính năng giúp kiểm tra và hỗ trợ lập trình viên phát hiện lỗi liên quan đến Type trong quá trình lập trình web.
Đọc bài viết sau đây để được giải đáp chi tiết hơn về:
- Tổng quan của Interface trong TypeScript và những tính năng nổi bật
- Cách Interface hoạt động trong TypeScript
- Cách sử dụng Interface trong TypeScript để tối ưu quá trình phát triển web
- Sự khác biệt giữa Interface và Type trong TypeScript
- Sự kết hợp giữa Interface với các framework khác như Angular và React
Định nghĩa Interface TypeScript là gì
Interface (hay còn gọi là giao diện) là một tính năng của TypeScript cho phép định nghĩa cấu trúc của một đối tượng. Interface sẽ cung cấp cách để mô tả hình dạng của đối tượng, bao gồm thuộc tính và phương thức của chúng mà không cần triển khai bất kỳ chức năng nào.
Interface chỉ tập trung vào các khía cạnh cấu trúc và kiểm tra kiểu, cho phép hiểu mã tốt hơn và xác thực trong quá trình phát triển. Cú pháp của Interface có thể được biểu diễn như sau:
interface InterfaceName { property1: type; property2: type; //Các thuộc tính và phương thức bổ sung có thể được định nghĩa ở đây }
Trong đó:
- interface: Được sử dụng để định nghĩa một giao diện.
- InterfaceName: Tên của giao diện theo quy ước đặt tên TypeScript.
- property1, property2: Thuộc tính của giao diện.
- type: Chú thích kiểu TypeScript định nghĩa kiểu của từng thuộc tính.
Để hiểu rõ hơn về Interface TypeScript, bạn có thể xem qua ví dụ sau:
interface Person { name: string; age: number; sex: "male" | "female"; } const personOne: Person = { name: "Coner", age: 24, sex: "male", } console.log(personOne.name); // Coner console.log(personOne.hobbies); // undefined
Trong ví dụ trên, trong khối mã bạn truy cập một thuộc tính được định nghĩa trong giao diện mà không có vấn đề gì bằng cách chạy console.log(personOne.name). Bạn cũng có thể thấy một ví dụ về việc cố gắng truy cập một thuộc tính không tồn tại trong giao diện bằng cách chạy console.log(personOne.hobbies), do đó đưa ra lỗi kiểu.
Xem thêm: TypeScript là gì? Top 20 câu hỏi và đáp án về TypeScript cần biết
Điểm nổi bật của Interface TypeScript
Một số lợi ích nổi bật của Interface trong TypeScript có thể kể đến như:
- Kiểm tra kiểu, làm nổi bật lỗi trong kiểu và vấn đề có thể xảy ra trong mã để ngăn lập trình viên truy cập vào bất kì thuộc tính nào có thể không tồn tại. Lưu ý thêm rằng việc kiểm tra kiểu chỉ diễn ra trong thời gian biên dịch (compile-time), không phải thời gian chạy (runtime).
- Định nghĩa và tạo hợp đồng rõ ràng cho các hàm hoặc mã sử dụng giao diện. Ngăn người dùng sử dụng các phương thức và thuộc tính không tồn tại, đảm bảo tuân thủ cấu trúc đã được thiết lập.
- Hoạt động như một dạng tài liệu giúp tăng cường khả năng đọc mã và giúp lập trình viên đọc mã hiểu cách thức hoạt động của nó cũng cách mã phù hợp với nhau.
- Định nghĩa các giao diện chung có thể được tái sử dụng và mở rộng trong toàn bộ ứng dụng. Bạn có thể đảm bảo tính nhất quán trong mã và logic của mình.
- Các IDE tích hợp với TypeScript có thể đọc các giao diện bạn định nghĩa và đưa ra các gợi ý tự động hoàn thành từ chúng. Đồng thời, hỗ trợ điều hướng mã để giúp bạn lập trình web năng suất và hiệu quả hơn.
- Giao diện giúp tái cấu trúc dễ dàng hơn, bạn có thể cập nhật việc triển khai một đoạn mã hoặc logic và đảm bảo tuân thủ cùng một giao diện.
Interface TypeScript hoạt động như thế nào?
Xác định cấu trúc đối tượng (Object Structure)
Ứng dụng cơ bản của giao diện TypeScript được thấy trong việc xác định cấu trúc đối tượng. Có thể tưởng tượng đến một kịch bản mà bạn được giao nhiệm vụ quản lý sẽ có nhiều hình dạng khác nhau trong một dự án.
Để hiểu rõ hơn về cách hoạt động của interface, bạn có thể xem qua ví dụ sau:
interface Shape { name: string; color: string; area(): number; } console.log("Use Case 1: Defining Object Structures"); console.log("-----------------------------------------"); function calculateArea(shape: Shape): void { console.log(`Calculating area of ${shape.name}...`); console.log(`Area: ${shape.area()}`); } const circle: Shape = { name: "Circle", color: "Red", area() { return Math.PI * 2 * 2; }, }; calculateArea(circle);
Kết quả hiển thị của đoạn mã trên.
Qua ví dụ trên, định nghĩa một giao diện có tên là Shape để biểu diễn cấu trúc của các hình dạng hình học. Giao diện Shape chứa các thuộc tính name và color, cả hai đều có kiểu chuỗi và phương thức area() trả về một số. Sau đó, định nghĩa một đối tượng hình tròn tuân theo giao diện Shape, chỉ định các thuộc tính của nó và triển khai phương thức area() để tính diện tích của nó.
Kiểm tra tham số hàm (Type-Check Function Parameters)
Một vai trò quan trọng khác của giao diện là kiểm tra kiểu tham số hàm trong thời gian biên dịch. Chẳng hạn ví dụ sau, xét một hàm được giao nhiệm vụ tính chu vi của shape:
function calculatePerimeter(shape: Shape): number { } console.log("\nUse Case 2: Type-Checking Function Parameters"); console.log("----------------------------------------------"); console.log("Calculating perimeter of a shape..."); console.log(`Perimeter: ${calculatePerimeter(circle)}`);
Kết quả hiển thị cho đoạn mã trên.
Ở ví dụ trên, Interface cho phép kiểm tra kiểu của tham số hàm. Hàm calculatePerimeter() sẽ lấy một đối tượng kiểu Shape làm tham số. Khi bạn cố gắng gọi hàm này với một đối tượng hình tròn, TypeScript sẽ đưa ra lỗi biên dịch vì đối tượng hình tròn không khớp chính xác với giao diện Shape mong đợi, đảm bảo an toàn kiểu trong quá trình phát triển.
Triển khai hợp đồng lớp (Implement Class Contract)
Trong ví dụ này, class Circle implements interface Shape, buộc phải triển khai đầy đủ các thuộc tính và phương thức được định nghĩa trong interface. TypeScript sẽ báo lỗi nếu class không implement đầy đủ các thành phần của interface:
class Circle implements Shape { constructor(public name: string, public color: string, public radius: number) {} area(): number { return Math.PI * this.radius * this.radius; } } console.log("\nUse Case 3: Implementing Class Contracts"); console.log("------------------------------------------"); const myCircle = new Circle("My Circle", "Blue", 3); console.log(`Area of ${myCircle.name}: ${myCircle.area()}`);
Kết quả hiển thị cho đoạn mã trên.
Ở ví dụ trên, trình bày giao diện cho phép kiểm tra kiểu của các tham số hàm. Có một hàm calculatePerimeter() lấy một đối tượng kiểu Shape làm tham số. Khi cố gắng gọi hàm này với một đối tượng hình tròn, TypeScript sẽ đưa ra lỗi biên dịch vì đối tượng hình tròn không khớp chính xác với giao diện Shape mong đợi, đảm bảo an toàn kiểu trong quá trình phát triển.
Cách sử dụng Interface trong TypeScript
Dưới đây là một số ứng dụng của Interface trong quá trình lập trình mã TypeScript.
Kiểu hàm (Function Type)
Bên cạnh định nghĩa kiểu đối tượng, bạn cũng có thể sử dụng giao diện để định kiểu cho các hàm, giá trị trả về cũng như đối số của chúng. Ví dụ như:
interface Args { name: string; age: number; } interface Return { name: string; age: number; doubledAge: number } function ageDoubler({name, age}: Args): Return { return { name, age, doubledAge: age * 2, } } const result = ageDoubler({ name: "John", age: 25 }); console.log(result); //{ name: "John", age: 25, doubledAge: 50 }
Lớp (Classes)
TypeScript có hỗ trợ gốc cho từ khóa class được triển khai trong ES2015. Bạn có thể định nghĩa một classes cũng như các trường và phương thức của nó như sau:
class Person { name: string = ''; age: number = 0; } const me = new Person();
Nhưng bạn có thể kết hợp các định nghĩa class này với giao diện để đảm bảo class triển khai đúng tất cả thuộc tính được định nghĩa trên giao diện.
interface PersonInt { name: string; age: number; } // Class 'Person' triển khai giao diện 'PersonInt' không đúng. // Thuộc tính 'age' bị thiếu trong kiểu 'Person' nhưng bắt buộc phải có trong kiểu 'PersonInt' class Person implements PersonInt { name: string = ''; } const me = new Person();
Trong ví dụ trên, bạn có một giao diện được gọi là PersonInt và sử dụng từ khóa implements để nói class Person sẽ có tất cả các kiểu được định nghĩa trong PersonInt. Điều này không đúng và thuộc tính “age” bị thiếu nên lỗi sẽ được hiển thị.
Thuộc tính tùy chọn (Optional Properties)
Khi làm việc với các đối tượng trong TypeScript, có một số thuộc tính chỉ có thể được định nghĩa trong một số trường hợp. Trong những trường hợp này, bạn có thể định nghĩa các thuộc tính tùy chọn (optional properties) như ví dụ sau:
interface Person { name: string; age: number; color?: string; } const person1: Person = { name: "John", age: 25 }; // Hợp lệ const person2: Person = { name: "Jane", age: 30, color: "blue" }; // Cũng hợp lệ
Khi sử dụng thuộc tính màu trên một đối tượng được gõ bằng giao diện Person, bạn sẽ phải tính đến trường hợp thuộc tính này có thể không có (thuộc tính này sẽ không được xác định nếu không được xác định).
Thuộc tính chỉ đọc (Readonly Properties)
Trong TypeScript, bạn có thể sử dụng tính năng Readonly với interfaces để đánh dấu một thuộc tính là chỉ đọc. Điều này có nghĩa là thuộc tính mục tiêu không thể được ghi vào trong quá trình kiểm tra kiểu mặc dù hành vi của nó không thay đổi trong thời gian chạy.
interface Person { readonly name: string; } const person: Person = { name: 'Coner', } function updateName(person: Person) { // Bạn không thể đọc 'person.name'. console.log(`name has the value '${person.name}'.`); // Nhưng bạn không thể gán lại nó // Không thể gán cho “name” vì đây là thuộc tính chỉ đọc. person.name = "hello"; }
Chữ ký (Index Signature)
Có thể có lúc bạn biết hình dạng của đối tượng, nhưng bạn không biết các thuộc tính thực tế của nó. Hoặc, các thuộc tính có thể thay đổi, nhưng hình dạng sẽ vẫn nhất quán. Điều này dẫn đến sự không thực tế hoặc không thể nhập từng thuộc tính trên giao diện. Để giải quyết, bạn có thể sử dụng chữ ký mục (index signature).
interface Index { [key: string]: boolean } // Ví dụ sử dụng const flags: Index = { isActive: true, isLoggedIn: false, hasPermission: true };
Giao diện này sẽ giúp bạn lập chỉ mục một đối tượng được nhập bằng giao diện Index với một chuỗi và sẽ có một boolean được trả về. Bạn cũng không bị giới hạn chỉ ở các kiểu boolean. Nó cũng có thể là một kiểu hoặc giao diện khác nếu bạn muốn, điều này rất tuyệt vời cho những lúc bạn không biết tất cả các thuộc tính nhưng biết hình dạng của chúng.
Ngoài ra, nếu bạn muốn kết hợp các chữ ký chỉ mục và các định nghĩa giao diện thông thường, bạn có thể làm như ví dụ sau:
interface Index { one: string; two: number; [key: string]: string | number | boolean }
Interface mở rộng (Extending Interfaces)
Extend Interface là tính năng giúp bạn mở rộng một giao diện hiện có và thêm các trường mới vào đó mà không thay đổi giao diện gốc, đồng thời có thể tạo một bản sao của nó. Ví dụ như:
interface Person { name: string; age: string; } interface PersonWithHobbies extends Person { hobbies: string[]; }
Giao diện Person gốc và mở rộng nó với thuộc tính hobby để tạo ra một giao diện mới có tên là PersonWithHobbies. Vì vậy, tại thời điểm này, bạn có hai giao diện, Person và PersonWithHobbies giống hệt nhau ngoại trừ giao diện sau có thêm thuộc tính hobby.
Nếu muốn, bạn cũng có thể kết hợp nhiều giao diện hiện có để tạo ra một giao diện mới mà không cần thêm bất kỳ thuộc tính mới nào vào giao diện đó, có thể thực hiện như sau.
interface Person { name: string; age: string; } interface Hobbies { hobbies: string[]; } interface PersonWithHobbies extends Person, Hobbies {}
Phân biệt hợp nhất (Discriminating Unions)
Phân biệt hợp nhất là cách có thể định nghĩa một kiểu mới từ nhiều giao diện và sử dụng một thuộc tính chung có trên tất cả các giao diện (gọi là phân biệt, discriminating) để phân biệt giữa các kiểu trong logic.
interface Circle { kind: "circle"; radius: number; } interface Square { kind: "square"; sideLength: number; } type Shape = Circle | Square; function getArea(shape: Shape): number { if (shape.kind === "circle") { // Giao diện hình tròn ở đây return Math.PI * shape.radius ** 2; } // Giao diện hình vuông ở đây return shape.sideLength ** 2; } const circle: Shape = { kind: "circle", radius: 5 }; const square: Shape = { kind: "square", sideLength: 4 }; console.log(getArea(circle)); // Kết quả hiển thị: 78.53981633974483 console.log(getArea(square)); // Kết quả hiển thị: 16
Ở ví trên, bạn có thể định nghĩa 2 giao diện (interfaces) cho hai hình dạng khác nhau (hình tròn và hình vuông). Sau đó, sử dụng một thuộc tính có trên cả hai giao diện (loại) để chỉ định giao diện nào chúng ta đang xử lý tại thời điểm đó.
Interface Generic
Generic cũng là một tính năng của TypeScript cho phép truyền nhiều loại dữ liệu khác nhau và tạo mã có thể tái sử dụng để xử lý các đầu vào khác nhau. Generics giống như một mẫu (template) có thể được sử dụng nhiều lần trên cùng một đoạn mã nhưng giá trị không phụ thuộc vào mỗi lần gọi hàm.
Generic Interface sẽ cho phép bạn kết hợp sức mạnh của Type với Interface để tạo ra các giao diện sử dụng chung một kiểu. Đồng thời, gán cùng một kiểu cho một hoặc nhiều thuộc tính được xác định trong giao diện.
interface Item<T> { id: number; value: T; timestamp: Date; } // Ví dụ sử dụng với nhiều kiểu dữ liệu const stringItem: Item<string> = { id: 1, value: 'Hello', timestamp: new Date() }; const numberItem: Item<number> = { id: 2, value: 42, timestamp: new Date() }; // Ví dụ với custom type interface User { name: string; email: string; } const userItem: Item<User> = { id: 3, value: { name: "John", email: "john@example.com" }, timestamp: new Date() };
Interface vs Type: Sự khác biệt và ưu nhược điểm
Giao diện (interface) và kiểu (type) đều được sử dụng để xác định hình dạng cũng như cấu trúc của mã. Tuy nhiên, cả hai có sự khác biệt trong cách sử dụng và tình huống mà một trong hai tính năng này sẽ hoạt động tốt hơn tính năng kia.
Dưới đây là một số điểm khác biệt giữa Interface và Type như sau:
Tính năng | Interface | Type |
Mục đích chính | Định nghĩa cấu trúc của objects và classes | Có thể định nghĩa cả union types, intersection types, primitives và tuples |
Chi tiết triển khai | Có thể dùng extends và implements | Chỉ có thể dùng intersection (&) |
Tính linh hoạt | Tốt hơn cho objects lớn vì TypeScript xử lý interface hiệu quả hơn đại diện. | Có thể chậm hơn với objects phức tạp |
Syntax | Chỉ có một cách khai báo. | Linh hoạt hơn trong cách khai báo |
Nhìn chung, Interface lý tưởng để mô tả hình dạng của đối tượng (object) và lớp (classes), thực thi hợp đồng cũng như duy trì tính nhất quán trong cấu trúc. Đồng thời, các kiểu dữ liệu rất tuyệt vời trong việc tạo ra các kiểu dữ liệu phức tạp, xử lý dữ liệu không phải đối tượng và định nghĩa kiểu dữ liệu cho thư viện bên ngoài.
Việc lựa chọn giữa interface và type trong TypeScript sẽ phụ thuộc vào nhu cầu cũng như tính chất dự án, cấu trúc mã bạn đang xử lý. Bạn có thể sử dụng cả hai trong cơ sở mã TypeScript để tận dụng thế mạnh của chúng trong các phần của ứng dụng.
Câu hỏi thường gặp về Interface TypeScript
Khi nào nên sử dụng Interface trong TypeScript?
Interface nên được sử dụng trong các trường hợp sau:
- Khi định nghĩa cấu trúc của objects và classes
- Khi cần declaration merging (mở rộng interface cùng tên)
- Khi làm việc với OOP và cần implement các contracts cho classes
- Khi định nghĩa public API của một module/library
- Khi cần tái sử dụng cấu trúc dữ liệu trong nhiều nơi
Điểm khác biệt chính giữa Type vs Interface trong TypeScript là gì?
Một số điểm khác biệt chính giữa Type và Interface bao gồm:
- Declaration Merging: Interface có thể mở rộng cùng tên, Type không thể
- Khả năng biểu diễn: Type có thể biểu diễn union types, tuples, primitives; Interface chủ yếu dùng cho objects
- Performance: Interface xử lý tốt hơn với objects phức tạp
- Cú pháp: Type có cú pháp linh hoạt hơn cho một số trường hợp đặc biệt
- Extends vs Intersection: Interface sử dụng extends, Type sử dụng intersection (&)
Tổng kết về Interface TypeScript
Bài viết đã cung cấp và giải đáp chi tiết về Interface TypeScript cũng như tính năng và cách sử dụng Interface để hoạt động hiệu quả trong TypeScript. Interface trong TypeScript như một công cụ mạnh mẽ giúp định nghĩa cấu trúc dữ liệu, đảm bảo an toàn cho kiểu (type) và thúc đẩy khả năng bảo trì mã. Bằng cách khai thác Interface, bạn có thể thiết lập các kỳ vọng cho cấu trúc đối tượng, tham số hàm hoặc hợp đồng lớp hợp lý.