TypeScript - Generics: A Beginner's Guide

Xin chào các bạn, những ngôi sao lập trình tương lai! Hôm nay, chúng ta sẽ bắt đầu một chuyến hành trình thú vị vào thế giới của TypeScript Generics. Đừng lo lắng nếu bạn là người mới bắt đầu lập trình - tôi sẽ là người hướng dẫn thân thiện của bạn, và chúng ta sẽ cùng nhau bước từng bước. Cuối cùng của bài hướng dẫn này, bạn sẽ sử dụng generics như một chuyên gia!

TypeScript - Generics

What Are Generics and Why Should We Care?

Trước khi chúng ta đi vào chi tiết, hãy bắt đầu với một ví dụ đơn giản. Hãy tưởng tượng bạn có một hộp ma thuật có thể chứa bất kỳ loại vật phẩm nào. Đôi khi bạn bỏ vào một cuốn sách, đôi khi là một玩具, hoặc thậm chí là một sandwich. Đó chính là generics trong TypeScript - chúng cho phép chúng ta tạo ra mã linh hoạt, có thể tái sử dụng và làm việc với nhiều loại khác nhau.

Problem Examples

Hãy nhìn vào một vài kịch bản mà generics có thể cứu nguy:

  1. Bạn muốn tạo một hàm có thể đảo ngược bất kỳ loại mảng nào (số, chuỗi, đối tượng).
  2. Bạn cần một lớp có thể lưu trữ và lấy lại bất kỳ loại dữ liệu nào.
  3. Bạn đang xây dựng một hàm tiện ích có thể hoạt động với nhiều loại dữ liệu khác nhau.

Nếu không có generics, bạn sẽ phải viết riêng từng hàm hoặc lớp cho mỗi loại dữ liệu. Đó là rất nhiều sự lặp lại, và như bất kỳ lập trình viên giỏi nào đều biết, sự lặp lại là kẻ thù của mã sạch!

TypeScript Generics to the Rescue!

Bây giờ, hãy c rolled up our sleeves và xem generics hoạt động như thế nào.

Basic Generic Function

Dưới đây là một hàm generic đơn giản có thể hoạt động với bất kỳ loại nào:

function identity<T>(arg: T): T {
return arg;
}

Hãy phân tích này:

  • <T> là tham số loại của chúng ta. Nó giống như một placeholder cho loại chúng ta sẽ sử dụng.
  • (arg: T) có nghĩa là hàm của chúng ta nhận một đối số loại T.
  • : T sau dấu ngoặc kép có nghĩa là hàm của chúng ta sẽ trả về một giá trị loại T.

Chúng ta có thể sử dụng hàm này như sau:

let output1 = identity<string>("Hello, Generics!");
let output2 = identity<number>(42);

console.log(output1); // "Hello, Generics!"
console.log(output2); // 42

Tuyệt vời phải không? Cùng một hàm hoạt động với nhiều loại khác nhau!

Generic Interface

Chúng ta cũng có thể sử dụng generics với các giao diện. Dưới đây là một ví dụ:

interface GenericBox<T> {
contents: T;
}

let stringBox: GenericBox<string> = { contents: "A secret message" };
let numberBox: GenericBox<number> = { contents: 123 };

console.log(stringBox.contents); // "A secret message"
console.log(numberBox.contents); // 123

Our GenericBox có thể chứa bất kỳ loại nội dung nào. Nó giống như hộp ma thuật chúng ta đã nói trước đó!

Generic Classes

Hãy tạo một lớp generic có thể hoạt động như một bộ nhớ dữ liệu đơn giản:

class DataStore<T> {
private data: T[] = [];

addItem(item: T): void {
this.data.push(item);
}

getItems(): T[] {
return this.data;
}
}

let stringStore = new DataStore<string>();
stringStore.addItem("Hello");
stringStore.addItem("World");
console.log(stringStore.getItems()); // ["Hello", "World"]

let numberStore = new DataStore<number>();
numberStore.addItem(1);
numberStore.addItem(2);
console.log(numberStore.getItems()); // [1, 2]

Lớp DataStore này có thể lưu trữ và lấy lại bất kỳ loại dữ liệu nào. Rất tiện lợi phải không?

Generic Constraints

Đôi khi, chúng ta muốn giới hạn các loại có thể sử dụng với generics. Chúng ta có thể làm điều này với các ràng buộc:

interface Lengthy {
length: number;
}

function logLength<T extends Lengthy>(arg: T): void {
console.log(arg.length);
}

logLength("Hello"); // 5
logLength([1, 2, 3]); // 3
logLength({ length: 10 }); // 10
// logLength(123); // Error: Number doesn't have a length property

Ở đây, hàm logLength của chúng ta chỉ có thể hoạt động với các loại có thuộc tính length.

Benefits of Generics

Bây giờ chúng ta đã thấy generics trong hành động, hãy tóm tắt lợi ích của chúng:

  1. Code Reusability: Viết một lần, sử dụng với nhiều loại.
  2. Type Safety: Lấy catch type-related errors at compile-time.
  3. Flexibility: Tạo các thành phần có thể hoạt động với nhiều loại dữ liệu khác nhau.
  4. Clarity: Làm rõ mối quan hệ giữa các đầu vào và đầu ra.

Generic Methods Table

Dưới đây là bảng handy của một số phương thức generic phổ biến mà bạn có thể gặp phải:

Method Description Example
Array.map<U>() Transforms array elements [1, 2, 3].map<string>(n => n.toString())
Promise.all<T>() Waits for all promises to resolve Promise.all<number>([Promise.resolve(1), Promise.resolve(2)])
Object.keys<T>() Gets object keys as an array Object.keys<{name: string}>({name: "Alice"})
JSON.parse<T>() Parses JSON string to object JSON.parse<{age: number}>('{"age": 30}')

Conclusion

Chúc mừng! Bạn vừa bước những bước đầu tiên vào thế giới kỳ diệu của TypeScript Generics. Nhớ rằng, như bất kỳ công cụ mạnh mẽ nào khác, generics có thể cảm thấy khó khăn ban đầu, nhưng với sự luyện tập, chúng sẽ trở thành bản năng thứ hai.

Khi bạn tiếp tục hành trình lập trình của mình, bạn sẽ thấy rằng generics giống như một瑞士军刀 trong hộp công cụ TypeScript của bạn - linh hoạt, mạnh mẽ và vô cùng hữu ích. Vậy nên, hãy tiếp tục lập trình, người bạn trẻ, và generics sẽ luôn đồng hành cùng bạn!

Chúc các bạn lập trình vui vẻ, và cho đến lần gặp lại, hãy tiếp tục khám phá và học hỏi!

Credits: Image by storyset