Sử dụng Generics để xử lý kiểu dữ liệu chung và tùy biến

Thumbnail Image

Generics là một tính năng rất hữu ích trong TypeScript, nó giúp chúng ta xử lý dữ liệu chung và tùy biến một cách dễ dàng và linh hoạt, giúp tăng tốc quá trình phát triển và giảm thiểu lỗi. Sau đây chúng ta sẽ đi tìm hiểu về generics và làm rõ vì sao nên sử dụng nó.

 

1. Giới thiệu về Generics

Generics  cung cấp một cách xử lý dữ liệu chung và tùy biến trong lập trình. Chúng ta có thể tạo một function hoặc class chung mà chúng ta có thể sử dụng cho nhiều loại dữ liệu khác nhau.

Ví dụ, chúng ta có thể tạo một hàm sắp xếp dữ liệu cho bất kỳ kiểu dữ liệu nào, bằng cách sử dụng Generics. Chúng ta chỉ cần chỉ định kiểu dữ liệu truyền vào khi gọi hàm đó, và nó sẽ hoạt động cho bất kỳ kiểu dữ liệu đó.

Cách sử dụng Generics trong TypeScript rất đơn giản, chúng ta cần khai báo Generics bằng cách sử dụng dấu <> và chỉ định tên cho Generics. Sau đó, chúng ta có thể sử dụng tên đó trong function hoặc class để xử lý dữ liệu chung và tùy biến.

Ví dụ minh họa:

// Sử dụng Generics trong hàm
function identity<T>(arg: T): T {
  return arg;
}

// Sử dụng Generics trong class
class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

// Sử dụng Generics trong interface
interface IGenericIdentityFn<T> {
  (arg: T): T;
}

2. Làm rõ vì sao nên sử dụng Generics

Mình có 2 đoạn code gần tương tự nhau, chỉ khác ở mỗi kiểu dữ liệu (number vs string).

Đoạn code 1:

function swapValuesInt(a: number, b: number): void {
    let temp = a;
    a = b;
    b = temp;
}

Đoạn code 2:

function swapValuesString(a: string, b: string): void {
    let temp = a;
    a = b;
    b = temp;
}

Như các bạn thấy 2 đoạn code này mình viết thực tế nó tương tự nhau, chỉ khác mỗi kiểu dữ liệu ở tham số truyền vào mà mình phải viết thành 2 đoạn riêng bỏ vào source code thì rất chuối và có hiện tượng lặp code, vì thế nên generics sinh ra để giải quyết vấn đề này. Mình viết đúng một function được sử dụng cho nhiều kiểu dữ liệu và mình truyền vào một kiểu dữ liệu chưa xác định, tạm gọi là T. Sau này mình chỉ cần khai báo kiểu dữ liệu lúc gọi hàm đó ra mà thôi.

Ví dụ: 

function swapValues<T>(a: T, b: T): void {
    let temp = a;
    a = b;
    b = temp;
}

let x = 10;
let y = 20;
swapValues<number>(x, y);
console.log(x); // 20
console.log(y); // 10

let name1 = "John";
let name2 = "Jane";
swapValues<string>(name1, name2);
console.log(name1); // Jane
console.log(name2); // John

Trong ví dụ trên, hàm "swapValues" sử dụng kiểu dữ liệu T để xác định kiểu dữ liệu của hai giá trị ab. Khi gọi hàm với giá trị số nguyên hoặc với giá trị chuỗi nó đều hoạt động tốt. Generics giúp cho hàm "swapValues" có thể tái sử dụng và sử dụng với bất kỳ kiểu dữ liệu nào.

Một ví dụ về một hàm dùng generics có 2 kiểu dữ liệu chưa xác định mình đặt tên là T và U.

function mergeObjects<T, U>(a: T, b: U): T & U {
  return {...a, ...b};
}

const merged = mergeObjects({name: "John"}, {age: 30});
console.log(merged);
// Output: {name: "John", age: 30}

Trong ví dụ trên, hàm mergeObjects sử dụng 2 kiểu dữ liệu Generics TU để xác định kiểu dữ liệu của 2 đối số ab mà hàm sẽ nhận vào. Khi gọi hàm, chúng ta chỉ cần truyền vào 2 đối số có cùng kiểu hoặc kiểu khác nhau, hàm sẽ tự động xác định kiểu dữ liệu từ đối số truyền vào và trả về kết quả có kiểu dữ liệu là T & U.