TypeScript - Lớp.Generic

Xin chào, các ngôi sao lập trình tương lai! Hôm nay, chúng ta sẽ khám phá thế giới đầy thú vị của Lớp.Generic trong TypeScript. Đừng lo lắng nếu bạn là người mới bắt đầu lập trình; tôi sẽ dẫn đường cho bạn từng bước một, giống như tôi đã làm cho hàng trăm học sinh trong những năm dạy học của mình. Vậy, hãy lấy饮料 yêu thích của bạn, ngồi thoải mái, và cùng nhau bắt đầu cuộc phiêu lưu này nhé!

TypeScript - Generic Classes

Lớp.Generic

Lớp.Generic là gì?

Hãy tưởng tượng bạn đang ở một cửa hàng kem, nhưng thay vì chọn hương vị, bạn đang chọn các kiểu dữ liệu. Đó chính là essence của các lớp generic! Chúng cho phép chúng ta tạo ra các thành phần linh hoạt, tái sử dụng được mà có thể hoạt động với các kiểu dữ liệu khác nhau mà không hy sinh tính an toàn của kiểu dữ liệu.

Hãy bắt đầu với một ví dụ đơn giản:

class Box<T> {
private content: T;

constructor(value: T) {
this.content = value;
}

getValue(): T {
return this.content;
}
}

Trong ví dụ này, Box là một lớp generic. <T> là như một placeholder cho một kiểu mà chúng ta sẽ xác định sau. Nó giống như nói với cửa hàng kem, "Tôi sẽ quyết định hương vị khi tôi đặt hàng!"

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

  • class Box<T>: Đây宣布 một lớp generic có tên Box với tham số kiểu T.
  • private content: T: Chúng ta đang nói rằng content sẽ là kiểu T, bất kể T là gì.
  • constructor(value: T): Constructor nhận một giá trị của kiểu T.
  • getValue(): T: Phương thức này trả về một giá trị của kiểu T.

Bây giờ, hãy xem cách chúng ta có thể sử dụng lớp này:

let numberBox = new Box<number>(42);
console.log(numberBox.getValue()); // Output: 42

let stringBox = new Box<string>("Hello, TypeScript!");
console.log(stringBox.getValue()); // Output: Hello, TypeScript!

Đó có phải là cool không? Chúng ta đã sử dụng cùng một lớp Box để lưu trữ cả số và chuỗi. Nó giống như có một hộp ma thuật có thể giữ bất cứ thứ gì bạn đặt vào nó, nhưng vẫn nhớ chính xác loại thứ gì đó đang giữ!

Nhiều Tham số Kiểu

Đôi khi, một tham số kiểu không đủ. Hãy tạo một ví dụ phức tạp hơn với nhiều tham số kiểu:

class Pair<T, U> {
private first: T;
private second: U;

constructor(first: T, second: U) {
this.first = first;
this.second = second;
}

getFirst(): T {
return this.first;
}

getSecond(): U {
return this.second;
}
}

Lớp Pair này có thể giữ hai giá trị có thể khác nhau về kiểu. Nó giống như có một cặp kem nơi mỗi phần có thể là một hương vị khác!

Hãy sử dụng lớp Pair của chúng ta:

let pair = new Pair<string, number>("Age", 30);
console.log(pair.getFirst());  // Output: Age
console.log(pair.getSecond()); // Output: 30

Ràng buộc Generic

Đôi khi, chúng ta muốn giới hạn các kiểu có thể được sử dụng với lớp generic của mình. Chúng ta có thể làm điều này bằng cách sử dụng các ràng buộc. Nó giống như nói, "Bạn có thể chọn bất kỳ hương vị kem nào, miễn là nó không quá cay!"

interface Lengthwise {
length: number;
}

class LengthChecker<T extends Lengthwise> {
checkLength(obj: T): string {
return `The length is: ${obj.length}`;
}
}

Trong ví dụ này, T extends Lengthwise có nghĩa là T phải là một kiểu có thuộc tính length. Hãy sử dụng nó:

let stringChecker = new LengthChecker<string>();
console.log(stringChecker.checkLength("Hello")); // Output: The length is: 5

let arrayChecker = new LengthChecker<number[]>();
console.log(arrayChecker.checkLength([1, 2, 3])); // Output: The length is: 3

// Điều này sẽ gây ra lỗi:
// let numberChecker = new LengthChecker<number>();
// Type 'number' does not satisfy the constraint 'Lengthwise'.

Thực hiện Interface Generic với Lớp Generic

Bây giờ, hãy nâng cao kỹ năng của chúng ta bằng cách thực hiện một interface generic với một lớp generic. Nó giống như tạo một công thức (interface) cho các loại kem khác nhau (classes)!

Trước hết, hãy định nghĩa một interface generic:

interface Repository<T> {
getById(id: number): T;
save(item: T): void;
}

Interface Repository này xác định một hợp đồng cho các lớp sẽ xử lý lưu trữ và truy xuất dữ liệu. Bây giờ, hãy thực hiện interface này với một lớp generic:

class GenericRepository<T> implements Repository<T> {
private items: T[] = [];

getById(id: number): T {
return this.items[id];
}

save(item: T): void {
this.items.push(item);
}
}

Lớp GenericRepository của chúng ta thực hiện interface Repository. Nó có thể hoạt động với bất kỳ kiểu T nào. Hãy sử dụng nó:

interface User {
name: string;
age: number;
}

let userRepo = new GenericRepository<User>();

userRepo.save({ name: "Alice", age: 30 });
userRepo.save({ name: "Bob", age: 25 });

console.log(userRepo.getById(0)); // Output: { name: "Alice", age: 30 }
console.log(userRepo.getById(1)); // Output: { name: "Bob", age: 25 }

Trong ví dụ này, chúng ta đã tạo một kho lưu trữ cho các đối tượng User. Nhưng điều kỳ diệu của việc thực hiện generic là chúng ta có thể dễ dàng tạo một kho lưu trữ cho bất kỳ loại nào khác!

Bảng Phương thức

Dưới đây là bảng tóm tắt các phương thức chúng ta đã đề cập:

Phương thức Mô tả Ví dụ
constructor(value: T) Tạo một instance mới của lớp generic new Box<number>(42)
getValue(): T Trả về giá trị lưu trữ trong lớp generic numberBox.getValue()
getFirst(): T Trả về giá trị đầu tiên trong một cặp pair.getFirst()
getSecond(): U Trả về giá trị thứ hai trong một cặp pair.getSecond()
checkLength(obj: T): string Kiểm tra độ dài của một đối tượng (với ràng buộc) stringChecker.checkLength("Hello")
getById(id: number): T Truy xuất một item từ kho lưu trữ theo ID userRepo.getById(0)
save(item: T): void Lưu một item vào kho lưu trữ userRepo.save({ name: "Alice", age: 30 })

Và thế là xong, các bạn! Chúng ta đã cùng nhau hành trình qua vùng đất của Lớp.Generic trong TypeScript, từ các hộp đơn giản đến các kho lưu trữ phức tạp. Nhớ rằng, thực hành là cách tốt nhất để trở nên hoàn hảo, vì vậy đừng ngần ngại thử nghiệm với các khái niệm này. Ai biết được? Bạn có thể tạo ra điều lớn lao tiếp theo trong lập trình! Hẹn gặp lại các bạn, chúc các bạn lập trình vui vẻ!

Credits: Image by storyset