Trong TypeScript, cả interfacesclasses đều có vai trò quan trọng trong việc xây dựng các cấu trúc mã mạnh mẽ và dễ bảo trì. Tuy nhiên, chúng phục vụ các mục đích khác nhau và việc lựa chọn giữa chúng phụ thuộc vào ngữ cảnh sử dụng. Để làm rõ, chúng ta cần hiểu kỹ về hai khái niệm này và khi nào nên sử dụng mỗi loại.

Interfaces: Khi Nào Nên Sử Dụng

Interface là một tính năng trong TypeScript cho phép bạn định nghĩa các hình dạng hoặc cấu trúc của một đối tượng. Nó chỉ mô tả dữ liệu và phương thức mà đối tượng nên có, nhưng không cung cấp logic thực thi. Interface không tạo ra các thể hiện (instance) của đối tượng và chỉ dùng để kiểm tra kiểu tại thời điểm biên dịch.

Khi nào sử dụng interface:

  1. Mô tả hình dạng của đối tượng: Khi bạn muốn mô tả cấu trúc dữ liệu của một đối tượng mà không cần quan tâm đến cách thức nó được thực thi. Interface hữu ích khi bạn cần các kiểu tĩnh hoặc hình dạng của dữ liệu.
  2. Tạo hợp đồng cho các lớp: Nếu bạn muốn một lớp phải tuân thủ một giao kèo nhất định, bạn có thể định nghĩa interface để đảm bảo rằng lớp đó thực hiện đầy đủ các thuộc tính và phương thức được yêu cầu.
  3. Đa kế thừa: Trong TypeScript, một lớp chỉ có thể kế thừa từ một lớp khác, nhưng có thể implements nhiều interfaces. Do đó, khi bạn cần một lớp có nhiều hành vi hoặc thuộc tính từ các nguồn khác nhau, bạn nên sử dụng interfaces.
  4. Mô hình hóa các cấu trúc phức tạp: Khi bạn làm việc với các cấu trúc dữ liệu phức tạp như API response, sử dụng interface để mô tả chính xác những gì cần thiết sẽ giúp mã của bạn rõ ràng và dễ quản lý hơn.

Ví dụ:

interface Person {
    name: string;
    age: number;
    greet(): void;
}

let john: Person = {
    name: "John",
    age: 30,
    greet: () => console.log("Hello!")
};

Trong ví dụ trên, interface Person chỉ định hình dạng của một đối tượng và đảm bảo rằng đối tượng john có đủ các thuộc tính như đã mô tả.

Classes: Khi Nào Nên Sử Dụng

Class (lớp) là một tính năng cơ bản trong lập trình hướng đối tượng, được sử dụng để tạo ra các đối tượng với cả dữ liệu (thuộc tính) và phương thức (hành vi). Classes trong TypeScript hỗ trợ tính kế thừa, khả năng đóng gói (encapsulation), và tính trừu tượng (abstraction).

Khi nào sử dụng class:

  1. Tạo đối tượng và logic liên quan: Khi bạn muốn định nghĩa cả cấu trúc dữ liệu và hành vi (logic) của một đối tượng. Class cung cấp một cách rõ ràng để quản lý cả thuộc tính và phương thức liên quan đến đối tượng đó.
  2. Tính đóng gói và kế thừa: Nếu bạn cần sử dụng các khái niệm hướng đối tượng như đóng gói (encapsulation) hoặc kế thừa, class là lựa chọn tốt. Bạn có thể tạo ra các lớp cơ bản và các lớp con mở rộng chúng, cho phép bạn tạo các cấu trúc phân cấp logic trong chương trình.
  3. Tạo các đối tượng có trạng thái: Khi bạn cần tạo ra các đối tượng với trạng thái thay đổi (biến thuộc tính hoặc phương thức), class là phù hợp. Mỗi instance của class có thể duy trì trạng thái riêng biệt.
  4. Khi cần phương thức khởi tạo: Class cho phép bạn định nghĩa một constructor để khởi tạo đối tượng, đây là điểm khác biệt so với interface, vốn chỉ mô tả cấu trúc nhưng không định nghĩa cách thức khởi tạo.

Ví dụ:

class Animal {
    name: string;
    
    constructor(name: string) {
        this.name = name;
    }

    speak(): void {
        console.log(`${this.name} makes a sound.`);
    }
}

const dog = new Animal("Dog");
dog.speak(); // Dog makes a sound.

Trong ví dụ này, Animal là một class có phương thức khởi tạo và hành vi (phương thức speak). Chúng ta có thể tạo ra nhiều instance của Animal với các trạng thái và hành vi riêng.

Khi Nào Sử Dụng Interface Và Class Cùng Lúc

TypeScript cho phép bạn sử dụng interface và class kết hợp với nhau. Ví dụ, bạn có thể sử dụng interface để định nghĩa các yêu cầu về kiểu và sau đó sử dụng class để triển khai chúng. Đây là cách giúp bạn duy trì một cấu trúc mạnh mẽ, đồng thời tận dụng cả kiểu tĩnh và các tính năng hướng đối tượng.

Ví dụ:

interface Shape {
    area(): number;
}

class Rectangle implements Shape {
    constructor(private width: number, private height: number) {}

    area(): number {
        return this.width * this.height;
    }
}

let rect: Shape = new Rectangle(10, 20);
console.log(rect.area()); // 200

Trong ví dụ này, Rectangle thực hiện (implements) interface Shape và triển khai phương thức area. Điều này giúp đảm bảo rằng tất cả các lớp thực hiện Shape đều có phương thức area.

Kết Luận

Interface nên được sử dụng khi bạn chỉ muốn định nghĩa cấu trúc hoặc yêu cầu cho dữ liệu, đặc biệt khi cần mô tả hình dạng của đối tượng hoặc khi cần đa kế thừa. Class nên được sử dụng khi bạn cần triển khai cả trạng thái lẫn hành vi của đối tượng và khi muốn tận dụng các tính năng của lập trình hướng đối tượng như đóng gói và kế thừa. Trong nhiều trường hợp, bạn có thể kết hợp cả hai để xây dựng mã rõ ràng, mạnh mẽ và dễ bảo trì.