Bài 11.2: Type aliases trong TypeScript

Trong hành trình làm quen và làm chủ TypeScript, bạn sẽ nhanh chóng nhận ra rằng việc định nghĩa các kiểu dữ liệu cho biến, tham số hay kết quả trả về là vô cùng quan trọng. Tuy nhiên, đôi khi các định nghĩa kiểu này có thể trở nên khá dài dòng, lặp đi lặp lại hoặc phức tạp khó hiểu.

May mắn thay, TypeScript cung cấp một công cụ mạnh mẽlinh hoạt để giải quyết vấn đề này: Type aliases, hay còn gọi là bí danh kiểu.

Type Aliases Là Gì?

Đơn giản mà nói, một type alias cho phép bạn đặt một tên mới cho một kiểu dữ liệu hiện có. Điều này giống như việc bạn đặt biệt danh cho một người – cái biệt danh đó vẫn ám chỉ đúng người đó, nhưng có thể ngắn gọn hoặc dễ nhớ hơn.

Cú pháp để khai báo một type alias rất đơn giản, sử dụng từ khóa type:

type TênKiểuMới = KiểuDữLiệuGốc;

Kiểu dữ liệu gốc ở đây có thể là bất kỳ kiểu nào trong TypeScript: kiểu nguyên thủy (string, number, boolean), kiểu đối tượng, kiểu union, kiểu intersection, kiểu tuple, kiểu function, thậm chí cả literal types.

Tại Sao Chúng Ta Nên Sử Dụng Type Aliases?

Sức mạnh thực sự của type aliases nằm ở khả năng cải thiện mã nguồn của bạn theo nhiều cách:

  1. Tăng Khả Năng Đọc: Thay vì lặp lại một cấu trúc kiểu phức tạp, bạn chỉ cần sử dụng tên alias ngắn gọný nghĩa. Điều này làm cho code dễ đọc và dễ hiểu hơn rất nhiều.
  2. Tái Sử Dụng: Định nghĩa một kiểu phức tạp một lần bằng alias và sử dụng nó ở nhiều nơi trong ứng dụng của bạn.
  3. Dễ Dàng Bảo Trì và Refactor: Nếu cần thay đổi định nghĩa của một kiểu phức tạp, bạn chỉ cần sửa đổi định nghĩa alias tại một chỗ duy nhất, và tất cả các nơi sử dụng alias đó sẽ tự động cập nhật theo.
  4. Quản Lý Độ Phức Tạp: Đối với các kiểu kết hợp từ nhiều kiểu khác (bằng union hoặc intersection), việc đặt tên cho chúng giúp chia nhỏ và quản lý độ phức tạp một cách hiệu quả.

Hãy cùng đi sâu vào các ví dụ để thấy rõ hơn những lợi ích này.

Các Ví Dụ Minh Họa Về Type Aliases

Type aliases có thể được sử dụng để đặt tên cho hầu hết mọi loại kiểu trong TypeScript. Dưới đây là một vài trường hợp phổ biến:

1. Alias cho Kiểu Đối Tượng (Object Types)

Đây là một trong những trường hợp sử dụng phổ biến nhất. Khi bạn có cấu trúc dữ liệu đối tượng phức tạp lặp lại, alias là cứu cánh.

// Trước khi dùng type alias
const user1: { id: number; name: string; email?: string; isActive: boolean } = {
  id: 1,
  name: "Alice",
  isActive: true
};

const user2: { id: number; name: string; email?: string; isActive: boolean } = {
  id: 2,
  name: "Bob",
  email: "bob@example.com",
  isActive: false
};

// Sau khi dùng type alias
type User = {
  id: number;
  name: string;
  email?: string; // Thuộc tính tùy chọn
  isActive: boolean;
};

const user3: User = {
  id: 3,
  name: "Charlie",
  isActive: true
};

const user4: User = {
  id: 4,
  name: "David",
  email: "david@example.com",
  isActive: false
};

// Giải thích:
// Chúng ta định nghĩa kiểu 'User' một lần với cấu trúc các thuộc tính của nó.
// Sau đó, chúng ta có thể sử dụng 'User' thay vì lặp lại cấu trúc đối tượng đầy đủ.
// Điều này làm code ngắn gọn và dễ đọc hơn rất nhiều.
2. Alias cho Kiểu Union (Union Types)

Kiểu union cho phép một biến có thể nhận nhiều loại kiểu khác nhau. Đặt tên cho kiểu union giúp làm rõ ý nghĩa của các giá trị có thể có.

// Trước khi dùng type alias
let status1: "pending" | "processing" | "completed" | "failed" = "pending";
status1 = "completed";
// status1 = "cancelled"; // -> Lỗi TS!

// Sau khi dùng type alias
type OrderStatus = "pending" | "processing" | "completed" | "failed";

let status2: OrderStatus = "processing";
status2 = "failed";
// status2 = "cancelled"; // -> Lỗi TS!

// Giải thích:
// Thay vì viết lại danh sách các trạng thái khả thi, chúng ta dùng alias 'OrderStatus'.
// Điều này không chỉ làm code gọn gàng mà còn truyền tải ý nghĩa rõ ràng:
// biến này biểu thị trạng thái của một đơn hàng.
3. Alias cho Kiểu Intersection (Intersection Types)

Kiểu intersection kết hợp nhiều kiểu thành một kiểu duy nhất yêu cầu đối tượng phải có tất cả các thuộc tính từ các kiểu đã kết hợp. Alias giúp đặt tên cho kiểu kết hợp này.

type Person = {
  name: string;
  age: number;
};

type Employee = {
  employeeId: string;
  department: string;
};

// Kết hợp Person và Employee bằng intersection
type EmployeeDetails = Person & Employee;

const newEmployee: EmployeeDetails = {
  name: "Eve",
  age: 30,
  employeeId: "E123",
  department: "Engineering"
};

// const incompleteEmployee: EmployeeDetails = { name: "Frank", age: 25 }; // -> Lỗi TS! Thiếu employeeId và department

// Giải thích:
// 'EmployeeDetails' là một alias cho kiểu intersection của 'Person' và 'Employee'.
// Bất kỳ đối tượng nào có kiểu 'EmployeeDetails' đều phải chứa TẤT CẢ các thuộc tính
// từ cả 'Person' và 'Employee'.
4. Alias cho Literal Types

Khi bạn muốn đặt tên cho một giá trị cụ thể (chuỗi, số, boolean) hoặc một tập hợp các giá trị cụ thể, alias rất hữu ích.

type AllowedHttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
type NumericStatusCode = 200 | 400 | 404 | 500;

let method: AllowedHttpMethod = 'GET';
let errorCode: NumericStatusCode = 404;

// method = 'OPTIONS'; // -> Lỗi TS!
// errorCode = 201;    // -> Lỗi TS!

// Giải thích:
// Alias giúp đặt tên ngữ nghĩa cho các giá trị cố định.
// 'AllowedHttpMethod' làm rõ rằng biến 'method' chỉ có thể nhận MỘT trong các giá trị chuỗi đó.
// 'NumericStatusCode' làm rõ biến 'errorCode' chỉ có thể nhận MỘT trong các giá trị số đó.
5. Alias cho Kiểu Hàm (Function Types)

Bạn có thể dùng type alias để định nghĩa chữ ký (signature) của một hàm, bao gồm kiểu của các tham số và kiểu của giá trị trả về. Điều này rất hữu ích khi làm việc với callback functions.

// Định nghĩa một alias cho kiểu hàm
type StringProcessor = (input: string) => string;

// Sử dụng alias để khai báo biến hàm
const processText: StringProcessor = (text) => {
  return text.toUpperCase();
};

const processTextAgain: StringProcessor = (str) => {
    return `Processed: ${str}`;
};

// const invalidProcessor: StringProcessor = (num: number) => num.toString(); // -> Lỗi TS! Kiểu tham số không đúng

// Giải thích:
// 'StringProcessor' là một alias cho bất kỳ hàm nào nhận MỘT tham số kiểu `string`
// và trả về MỘT giá trị kiểu `string`.
// Điều này giúp dễ dàng khai báo các biến hoặc tham số hàm cần có chữ ký cụ thể.
6. Alias cho Kiểu Tuple (Tuple Types)

Tuple là một mảng có kích thước cố định, trong đó kiểu của các phần tử tại các vị trí cụ thể là đã biết. Alias giúp đặt tên ý nghĩa cho cấu trúc tuple này.

// Định nghĩa một alias cho kiểu tuple
type Point = [number, number]; // Tuple với 2 phần tử: x và y (cả hai đều là số)

const origin: Point = [0, 0];
const location: Point = [15, 30];

// const invalidPoint: Point = [10]; // -> Lỗi TS! Kích thước không đúng
// const anotherInvalidPoint: Point = [5, "ten"]; // -> Lỗi TS! Kiểu phần tử không đúng

// Giải thích:
// 'Point' là một alias cho một tuple luôn có 2 phần tử, cả hai đều là số.
// Điều này làm cho mục đích của tuple [x, y] rõ ràng hơn.
Type Aliases và Interfaces: Khi Nào Sử Dụng Cái Nào?

Một câu hỏi thường gặp là: Type aliases và interfaces khác nhau như thế nào, và khi nào nên sử dụng cái nào?

  • Interfaces chủ yếu được sử dụng để định nghĩa hình dạng (shape) của các đối tượngcác lớp (classes), cũng như để định nghĩa chữ ký của hàm (như một property). Interfaces có khả năng mở rộng (extends) và có thể được triển khai (implements) bởi các class. Một điểm đặc biệt của interfaces là Declaration Merging – nếu bạn khai báo hai interface cùng tên, TypeScript sẽ tự động hợp nhất chúng lại.
  • Type Aliases linh hoạt hơn nhiều: chúng có thể đặt tên cho bất kỳ kiểu nào, bao gồm kiểu nguyên thủy, union, intersection, tuple, literal types, bên cạnh kiểu đối tượng và hàm. Tuy nhiên, type aliases không có khả năng Declaration Merging và không thể được implements bởi các class theo cách trực tiếp như interfaces.

Quy tắc chung:

  • Sử dụng Interfaces khi bạn định nghĩa hình dạng của một đối tượng hoặc một class, đặc biệt nếu bạn muốn tận dụng tính năng Declaration Merging hoặc cần implements nó.
  • Sử dụng Type Aliases khi bạn định nghĩa bất kỳ kiểu nào khác kiểu đối tượng, ví dụ: union types, intersection types, primitive aliases, tuple types, function signatures, hoặc khi bạn muốn đặt tên cho một literal type. Đối với kiểu đối tượng đơn giản, cả hai đều hoạt động tốt, sự lựa chọn đôi khi phụ thuộc vào quy ước của team.

Comments

There are no comments at the moment.