TypeScript Generics cho phép lập trình viên viết code có thể tái sử dụng với nhiều kiểu dữ liệu khác nhau mà vẫn đảm bảo an toàn kiểu. Chúng rất cần thiết để xây dựng các ứng dụng TypeScript mạnh mẽ và có khả năng mở rộng.
Tổng quan về TypeScript Generics
Để đảm bảo code minh bạch và dễ quản lý, Typescript yêu cầu việc quản lý an toàn và hiệu quả nhiều loại dữ liệu. Một trong những tính năng cốt lõi của Typescript là generics, cho phép tạo các hàm, lớp và interface vẫn tuân thủ các ràng buộc kiểu nghiêm ngặt. Generics cho phép bạn viết ít code hơn, ít lỗi hơn và quan trọng nhất là xây dựng các thành phần linh hoạt cho các kiểu dữ liệu khác nhau.
Bài viết này khám phá những điều cần thiết về TypeScript generics, bao gồm cách sử dụng chúng trong hàm, lớp và interface, đồng thời minh họa cách chúng làm cho code linh hoạt và mạnh mẽ.
Generic Typescript là gì?
Generics trong TypeScript cho phép định nghĩa code với các kiểu giữ chỗ, cho phép code linh hoạt, mở rộng và tái sử dụng được trong khi vẫn đảm bảo an toàn kiểu. TypeScript thực hiện kiểm tra an toàn kiểu trong thời gian biên dịch như một trình giữ chỗ xác định kiểu generic. Khi thành phần được triển khai, kiểu thực tế sẽ thay thế trình giữ chỗ.
Kỹ thuật này giúp việc quản lý và giảm trùng lặp dễ dàng hơn vì bạn không cần triển khai riêng biệt cho từng kiểu dữ liệu. Nếu không có generics, bạn sẽ phải viết nhiều phiên bản của một hàm hoặc lớp để xử lý các kiểu dữ liệu khác nhau, dẫn đến trùng lặp code. Generics cho phép một triển khai duy nhất có thể tái sử dụng cho nhiều loại khác nhau trong khi vẫn giữ được kiểm tra kiểu tĩnh.
Các ví dụ code trong phần tiếp theo sẽ giúp bạn hiểu sự khác biệt này.
Khi nào nên sử dụng Typescript generic?
Generics có thể được sử dụng trên các phần khác nhau của TypeScript để giúp quản lý các kiểu hiệu quả hơn. Chúng rất hữu ích trong các hàm, interface, lớp và các cấu trúc khác, nơi tính linh hoạt là rất quan trọng.
1. Kiểu Generic trong hàm
Generics thường được áp dụng trong các hàm để giảm trùng lặp. Ví dụ: hãy xem xét một hàm nhận một chuỗi hoặc một số làm tham số.
function identity(value: any): any { return value;
}
const result1 = identity(42); // result1: any
const result2 = identity("hello"); // result2: any
Hàm này hoạt động tốt. Nhưng nó sử dụng kiểu any, có nghĩa là TypeScript mất dấu kiểu cụ thể. Kết quả là, giá trị trả về được gõ là any và TypeScript không thể thực thi an toàn kiểu nữa. Nếu chúng ta cần duy trì an toàn kiểu, chúng ta sẽ phải viết hai hàm khác nhau, một hàm trả về một chuỗi trong khi hàm kia trả về một số.
Tuy nhiên, cách tiếp cận đó sẽ làm tăng trùng lặp code. Chúng ta có thể cải thiện hàm trên bằng cách sử dụng generics để bảo toàn thông tin kiểu.
function identity<T>(value: Type): T { return value;
}
const result1 = identity<number>(42); // result1: number
const result2 = identity<string>("hello"); // result2: string
T đại diện cho kiểu mà phương thức sử dụng trong trường hợp này. Nếu có, TypeScript sẽ xác nhận rằng kiểu đầu vào và kiểu trong tham số trả về là giống nhau.
Ngoài ra, chúng ta có thể định nghĩa hàm mà không cần định nghĩa rõ ràng kiểu tham số.
const result3 = identity(100); // result3: number
const result4 = identity("world"); // result4: string
Trong TypeScript, bạn có thể sử dụng nhiều hơn một tham số kiểu generic khi làm việc với nhiều kiểu trong một hàm hoặc thành phần duy nhất.
Ví dụ: bạn có thể muốn một hàm nhận hai kiểu đầu vào khác nhau và trả về chúng dưới dạng một cặp.
function multipleParams<T, U>(first: T, second: U): [T, U] { return [first, second];
}
const result1 = multipleParams<string, number>("hello", 42); // result1: [string, number]
const result2 = multipleParams<string, number>("hello", "world"); // result2: gives a type error
Trong trường hợp này, hàm trả về một tuple với phần tử đầu tiên thuộc kiểu T và phần tử thứ hai thuộc kiểu U. Điều này cho phép hàm xử lý an toàn kiểu của hai kiểu riêng biệt.
2. Các kiểu mặc định trong TypeScript
Trong TypeScript, bạn có thể cung cấp một kiểu mặc định cho một generic, làm cho nó trở thành tùy chọn. Nếu không có kiểu nào được cung cấp, TypeScript sẽ sử dụng kiểu mặc định.
function createArray<T = string>(length: number, value: T): T[] { return Array(length).fill(value);
} const stringArray = createArray(3, "hello"); // T defaults to string, so stringArray is a string array
const numberArray = createArray<number>(3, 42); // T is explicitly set to a number, so numberArray is a number array
Trong ví dụ này, tham số kiểu T mặc định là string. Nếu nhà phát triển không chỉ ra một kiểu cụ thể khi họ gọi hàm, T sẽ là một chuỗi theo mặc định.
3. Generic Interfaces
TypeScript generics cũng có thể được áp dụng cho các interface. Hãy tưởng tượng bạn muốn định nghĩa một interface Box với một giá trị thuộc kiểu any.
interface Box { value: any;
}
const numberBox: Box = { value: 123 }; // correct
const stringBox: Box = { value: "hello" }; // correct
Điều này tương đương với ví dụ về hàm generic; code này cũng sẽ hoạt động mà không có vấn đề gì vì chúng ta chưa định nghĩa một kiểu cụ thể. Tuy nhiên, vì giá trị được gõ là any, chúng ta có thể gặp phải lỗi liên quan đến kiểu.
Để đảm bảo kiểu, chúng ta có thể định nghĩa một generic interface ở đây.
interface Box<Type> { value: Type;
}
const numberBox: Box<number> = { value: 123 }; // number
const stringBox: Box<string> = { value: "hello" }; // string
const stringBox2: Box<string> = { value: 123 }; // incorrect
Interface này là generic và kiểu giá trị của nó bị ràng buộc chặt chẽ với biến Type. Biến Type có thể được chỉ định là số hoặc chuỗi trong khi tạo một instance để TypeScript đảm bảo rằng các kiểu phù hợp được tuân thủ.
4. Các lớp chung
Các lớp cũng có thể được viết bằng generic để xử lý các kiểu khác nhau trong khi vẫn duy trì tính an toàn của kiểu. Hãy tạo một lớp Storage có thể lưu trữ và truy xuất các giá trị của bất kỳ kiểu nào.
class Storage { private data: any; setItem(item: any): void { this.data = item; } getItem(): any { return this.data; }
}
const storage = new Storage();
storage.setItem(123);
const item = storage.getItem();
Lớp này hoạt động, nhưng vì dữ liệu có kiểu any, phương thức getItem trả về any, loại bỏ tính an toàn của kiểu. Vì vậy, chúng ta có thể viết lại lớp bằng cách sử dụng generic để cải thiện tính an toàn của kiểu.
class Storage<T> { private data: T; setItem(item: T): void { this.data = item; } getItem(): T { return this.data; }
}
const numberStorage = new Storage<number>();
numberStorage.setItem(123);
const item = numberStorage.getItem();
Trong trường hợp này, kiểu T được lớp Storage sử dụng . Typescript đảm bảo dữ liệu là chính xác khi bạn định nghĩa kiểu cho chúng khi bạn tạo một thể hiện. Phương thức getItem trong ví dụ mã này sẽ tạo ra một số.
5. Ràng buộc chung
Bạn có thể sử dụng ràng buộc chung để hạn chế các kiểu mà một ràng buộc chung có thể chấp nhận, đảm bảo chúng có các thuộc tính cụ thể.
Ví dụ, nếu bạn có một hàm cần truy cập thuộc tính length của đầu vào, bạn có thể sử dụng ràng buộc để đảm bảo chỉ các kiểu có thuộc tính length mới được phép. Điều này ngăn Typescript đưa ra lỗi hoặc để các kiểu không tương thích lọt qua.
function printLength<T>(value: T): void { console.log(value.length); // Error: Typescript doesn’t know if the value has length }
Ở đây, giá trị T không được xác định bằng thuộc tính length . Để bỏ qua vấn đề này, chúng ta có thể thêm một ràng buộc chỉ định rằng T phải có thuộc tính length . Chúng ta thực hiện điều này bằng cách nói T extends { length: number } .
function printLength<T extends { length: number}>(value: T): void { console.log(value.length); // Now Typescript knows length exists
} printLength("hello"); // Output: 5
printLength([1, 2, 3]); // Output: 3
printLength({ length: 10 }); // Output: 10
Bây giờ, hàm này sẽ có thuộc tính độ dài; nó sẽ không đưa ra bất kỳ lỗi nào và sẽ thực thi với độ dài của đầu vào.
Phần kết luận
Các generic Typescript cho phép bạn viết mã linh hoạt, có thể tái chế và an toàn về kiểu. Bạn có thể quản lý nhiều kiểu dữ liệu mà không cần lặp lại mã bằng cách sử dụng các lớp, phương thức và giao diện với các generic này.
Các ràng buộc chung, nhiều kiểu và các kiểu mặc định là một số trường hợp sử dụng chính mà chúng tôi đã xem xét trong bài đăng này và cho thấy cách mỗi trường hợp có thể cải thiện khả năng mở rộng và khả năng bảo trì của chương trình.
Hiểu về các kiểu generic của Typescript có thể giúp bạn viết mã chính xác hơn, dễ thích ứng hơn và an toàn hơn, giúp ứng dụng Typescript của bạn mạnh mẽ hơn.
Cảm ơn các bạn đã theo dõi!