Sử dụng hàm generics trong TypeScript
Hàm generics là một tính năng quan trọng cho phép bạn làm việc với nhiều kiểu dữ liệu mà không cần phải viết lại mã nguồn nhiều lần. Điều này giúp tạo ra các hàm linh hoạt, tái sử dụng, và giảm thiểu sự trùng lặp trong mã nguồn.
Trong phần này, mình sẽ tìm hiểu cú pháp cơ bản của hàm generics, cách sử dụng chúng trong thực tế, và những ví dụ cụ thể về cách hàm generics có thể giải quyết các vấn đề thực tế. Hãy cùng tìm hiểu khía cạnh quan trọng này của TypeScript để nâng cao khả năng phát triển mã nguồn của bạn.
Generics là gì?
Hàm generics, thường được gọi là generics trong TypeScript và nhiều ngôn ngữ lập trình khác, là một tính năng cho phép bạn làm việc với nhiều kiểu dữ liệu khác nhau trong một cách tổng quát và tái sử dụng mã nguồn. Generics cho phép bạn tạo các hàm, lớp, và cấu trúc dữ liệu có thể hoạt động với nhiều kiểu dữ liệu mà bạn không cần biết trước.
Ở ngôn ngữ lập trình TypeScript, generics được thể hiện bằng sử dụng type parameter, tức là bạn định nghĩa một biến kiểu (type variable) trong hàm hoặc lớp và sau đó sử dụng biến kiểu này để làm việc với dữ liệu. Type parameter cho phép bạn áp dụng các kiểu dữ liệu khác nhau tùy thuộc vào cách hàm hoặc lớp được gọi.
Bài viết này được đăng tại [free tuts .net]
Dưới đây là một ví dụ đơn giản về cách sử dụng hàm generics trong TypeScript để đảo ngược một mảng:
function daoNguocMang<T>(mang: T[]): T[] { return mang.reverse(); } const mangSo: number[] = [1, 2, 3, 4, 5]; const mangChuoi: string[] = ["apple", "banana", "cherry"]; const mangSoDaoNguoc = daoNguocMang(mangSo); // [5, 4, 3, 2, 1] const mangChuoiDaoNguoc = daoNguocMang(mangChuoi); // ["cherry", "banana", "apple"]
Trong ví dụ này, T là type parameter, và mình sử dụng nó để định nghĩa kiểu dữ liệu của mảng đầu vào và đầu ra. Hàm daoNguocMang có thể hoạt động với mảng của bất kỳ kiểu dữ liệu nào (số, chuỗi, v.v.) mà bạn cung cấp cho nó.
Tính năng generics giúp bạn viết mã nguồn linh hoạt và tái sử dụng, giảm thiểu sự trùng lặp và giúp quản lý mã nguồn một cách hiệu quả khi bạn phải làm việc với nhiều kiểu dữ liệu khác nhau.
Cú pháp cơ bản của Generics
Trong phần này, mình sẽ tìm hiểu cú pháp cơ bản của hàm generics trong TypeScript và tìm hiểu cách khai báo hàm generic và sử dụng type parameter.
Khai báo hàm generic
Khi bạn muốn tạo một hàm generic trong TypeScript, bạn sử dụng type parameter. Type parameter
là một biến kiểu (type variable) đặt tên bởi bạn, thường được ký hiệu bằng một chữ cái viết hoa (ví dụ: T, U, E, ...). Type parameter được đặt trong cặp dấu < và > sau tên hàm.
Ví dụ, đây là cú pháp khai báo một hàm generic đơn giản:
function tenHam<T>(thamSo: T): T { // Thực hiện các phép toán với thamSo return thamSo; }
Trong ví dụ này, T
là type parameter và hàm tenHam
có thể hoạt động với bất kỳ kiểu dữ liệu nào.
Sử dụng type parameter
Sau khi bạn đã khai báo type parameter, bạn có thể sử dụng chúng trong hàm để thực hiện các phép toán hoặc kiểm tra kiểu dữ liệu của tham số và giá trị trả về.
Ví dụ sau đây sử dụng type parameter để kiểm tra kiểu dữ liệu của tham số và trả về giá trị:
function soLanLapLai<T>(mang: T[], phanTu: T): number { let soLan = 0; for (let i = 0; i < mang.length; i++) { if (mang[i] === phanTu) { soLan++; } } return soLan; } const mangSo: number[] = [1, 2, 2, 3, 4, 2, 5]; const soLanLapLaiSo2 = soLanLapLai(mangSo, 2); // Kết quả là 3
Trong ví dụ này, T là type parameter, và mình sử dụng nó để kiểm tra kiểu dữ liệu của mảng và phần tử phanTu.
Hàm generics cho phép bạn tạo các hàm tổng quát có thể làm việc với nhiều kiểu dữ liệu khác nhau và nó giúp bạn viết mã nguồn hiệu quả và tái sử dụng.
Sử dụng Generics trong thực tế
Trong phần này, mình sẽ thực hiện ba ví dụ cụ thể về việc sử dụng hàm generics để giải quyết các vấn đề thực tế.
Hàm Array Reverse sử dụng Generics
Mình sẽ tạo một hàm generics để đảo ngược thứ tự các phần tử trong một mảng.
function daoNguocMang<T>(mang: T[]): T[] { return mang.reverse(); } const mangSo: number[] = [1, 2, 3, 4, 5]; const mangChuoi: string[] = ["apple", "banana", "cherry"]; const mangSoDaoNguoc = daoNguocMang(mangSo); // Kết quả: [5, 4, 3, 2, 1] const mangChuoiDaoNguoc = daoNguocMang(mangChuoi); // Kết quả: ["cherry", "banana", "apple"]
Hàm daoNguocMang
có thể hoạt động với mảng của bất kỳ kiểu dữ liệu nào, từ số đến chuỗi.
Hàm Array Filter sử dụng Generics
Ta sẽ tạo một hàm generic để lọc các phần tử của mảng dựa trên một điều kiện nhất định.
function locMang<T>(mang: T[], dieuKien: (phanTu: T) => boolean): T[] { return mang.filter(dieuKien); } const mangSo: number[] = [1, 2, 3, 4, 5]; const soLe = locMang(mangSo, so => so % 2 === 1); // Kết quả: [1, 3, 5] const mangChuoi: string[] = ["apple", "banana", "cherry"]; const chuoiCoKyTuE = locMang(mangChuoi, chuoi => chuoi.includes("e")); // Kết quả: ["apple", "cherry"]
Hàm locMang
cho phép bạn lọc mảng bằng cách truyền một hàm điều kiện cho việc lọc.
Hàm Queue Generic
Mình sẽ xây dựng một cấu trúc dữ liệu hàng đợi (queue) sử dụng hàm generics.
class HangDoi<T> { private mangPhanTu: T[] = []; them(phanTu: T): void { this.mangPhanTu.push(phanTu); } lay(): T | undefined { if (this.khongRong()) { return this.mangPhanTu.shift(); } return undefined; } kichThuoc(): number { return this.mangPhanTu.length; } khongRong(): boolean { return this.mangPhanTu.length > 0; } } const hangDoiSo = new HangDoi<number>(); hangDoiSo.them(1); hangDoiSo.them(2); const phanTuDau = hangDoiSo.lay(); // Kết quả: 1 const hangDoiChuoi = new HangDoi<string>(); hangDoiChuoi.them("apple"); hangDoiChuoi.them("banana"); const phanTuDauChuoi = hangDoiChuoi.lay(); // Kết quả: "apple"
Hàm HangDoi
cho phép bạn tạo một hàng đợi với bất kỳ kiểu dữ liệu nào. Hàng đợi này hoạt động theo nguyên tắc "FIFO" (First-In-First-Out), tức là phần tử được thêm vào đầu tiên sẽ được lấy ra đầu tiên.
Ràng buộc (Constraints) trong Generics
Ràng buộc (constraints) trong hàm generics cho phép bạn áp dụng các ràng buộc hoặc giới hạn đối với type parameter. Điều này giúp kiểm tra và giới hạn kiểu dữ liệu mà type parameter có thể nhận. Dưới đây, mình sẽ tìm hiểu về hai loại ràng buộc quan trọng trong TypeScript.
Ràng buộc kiểu dữ liệu
Khi bạn muốn type parameter phải là một kiểu cụ thể hoặc phải thỏa mãn một điều kiện nào đó, bạn có thể sử dụng ràng buộc kiểu dữ liệu. Dưới đây là ví dụ:
function timPhanTuLonNhat<T>(mang: T[]): T { return Math.max(...mang); // Lỗi! TypeScript không biết làm thế nào với type parameter T }
Trong trường hợp này, TypeScript không biết cách xử lý type parameter T vì nó có thể là bất kỳ kiểu dữ liệu nào. Để giải quyết vấn đề này, bạn có thể sử dụng ràng buộc kiểu dữ liệu để chỉ định rằng T phải là một kiểu dữ liệu có thể so sánh.
function timPhanTuLonNhat<T extends Comparable>(mang: T[]): T { return Math.max(...mang); // Bây giờ TypeScript biết rằng T phải là một kiểu có thể so sánh } // Ví dụ sử dụng: const mangSo: number[] = [1, 2, 3, 4, 5]; const soLonNhat = timPhanTuLonNhat(mangSo); // Kết quả: 5
Ở ví dụ trên, T được giới hạn bởi extends Comparable
, điều này đảm bảo rằng type parameter T phải là một kiểu dữ liệu có thể so sánh (ví dụ: số).
Ràng buộc cho số lần gọi Generic
Bạn cũng có thể giới hạn số lần mà hàm generic có thể được gọi trong hàm. Điều này có thể đảm bảo rằng bạn không sử dụng hàm generic quá nhiều lần trong một lúc.
function gopMang<T>(mang1: T[], mang2: T[]): T[] { // Làm việc với hai mảng và trả về một mảng kết quả } function gopHaiMang<T>(mang1: T[], mang2: T[]): T[] { return gopMang(gopMang(mang1, mang2), mang1); // Gọi hàm generic quá nhiều lần } // Sử dụng ràng buộc để giới hạn số lần gọi hàm generic function gopHaiMang<T>(mang1: T[], mang2: T[]): T[] { if (mang1.length + mang2.length > 100) { throw new Error("Số lượng phần tử quá lớn."); } return gopMang(mang1, mang2); }
Trong ví dụ trên, hàm gopHaiMang sử dụng ràng buộc để kiểm tra số lượng phần tử trong hai mảng trước khi gọi hàm generic gopMang. Điều này đảm bảo rằng hàm generic không bị gọi quá nhiều lần.
Ràng buộc trong hàm generics giúp kiểm tra và quản lý kiểu dữ liệu và số lần gọi hàm generic một cách an toàn trong TypeScript.
Ví dụ cụ thể của Generics
Trong phần này, mình sẽ xem xét một số ví dụ cụ thể về cách sử dụng hàm generics để giải quyết các vấn đề thực tế và phân tích cách chúng đóng góp vào tính linh hoạt và tái sử dụng của mã nguồn.
Ví dụ: Generic cho tính toán số lớn
Một ứng dụng khác là sử dụng generics để tạo một hàm cho tính toán số lớn (big numbers) dựa trên thư viện số lớn nào đó.
function congHaiSoLon<T>(a: T, b: T): T { return a.add(b); } const so1 = new BigNumber("12345678901234567890"); const so2 = new BigNumber("98765432109876543210"); const ketQua = congHaiSoLon(so1, so2);
Trong ví dụ này, mình sử dụng generics để tạo hàm congHaiSoLon, cho phép mình tính toán với số lớn sử dụng thư viện số lớn nào đó mà không cần viết lại hàm cho từng kiểu dữ liệu số lớn.
Ví dụ: Generic cho quản lý thư viện dữ liệu
Generics cũng có thể được sử dụng để quản lý thư viện dữ liệu. Mình có thể tạo một cơ sở dữ liệu generic cho nhiều loại đối tượng.
class CoSoDuLieu<T> { private duLieu: T[] = []; themMoi(duLieuMoi: T): void { this.duLieu.push(duLieuMoi); } layTatCa(): T[] { return this.duLieu; } } const coSoDuLieuNguoiDung = new CoSoDuLieu<NguoiDung>(); coSoDuLieuNguoiDung.themMoi({ id: 1, ten: "Alice" }); const coSoDuLieuSanPham = new CoSoDuLieu<SanPham>(); coSoDuLieuSanPham.themMoi({ maSanPham: "12345", tenSanPham: "Bút bi" });
Trong ví dụ này, mình sử dụng generics để tạo một cơ sở dữ liệu có thể lưu trữ nhiều loại đối tượng khác nhau mà không cần viết lại cơ sở dữ liệu cho từng loại đối tượng.
Như bạn có thể thấy, hàm generics cung cấp tính linh hoạt và tái sử dụng mạnh mẽ trong việc giải quyết các vấn đề thực tế trong phát triển phần mềm. Chúng giúp bạn viết mã nguồn sạch sẽ và hiệu quả mà không cần lặp lại công việc.
Kết bài
Trong bài viết này, mình đã tìm hiểu một loạt các khía cạnh liên quan đến hàm generics trong TypeScript.Ta đã bắt đầu bằng việc hiểu về cú pháp cơ bản của hàm generics, cách khai báo và sử dụng type parameter.
Tiếp theo, mình đã tìm hiểu về các khía cạnh quan trọng bao gồm hàm generics, khả năng sử dụng type parameter, và cách chúng đóng góp vào tính linh hoạt và tái sử dụng của mã nguồn.
Cuối cùng, mình đã xem xét các ví dụ cụ thể về cách sử dụng hàm generics trong các tình huống thực tế, bao gồm xử lý dữ liệu API, tính toán số lớn, và quản lý thư viện dữ liệu. Những ví dụ này đã minh họa sức mạnh và ứng dụng thực tế của hàm generics trong TypeScript.
Hàm generics là một công cụ mạnh mẽ để viết mã nguồn linh hoạt, tái sử dụng và hiệu quả trong TypeScript. Việc hiểu và sử dụng chúng sẽ cải thiện khả năng phát triển và quản lý mã nguồn của bạn.