+8

Generics trong TypeScript

Lời đầu tiên, xin chúc cộng đồng Viblo bước sang năm mới gặt hái thêm nhiều thành công mới.

TypeScript là một ngôn ngữ mã nguồn mở dựa trên JavaScript, một trong những ngôn ngữ phổ biến và được sử dụng nhiều nhất. Typescript mở rộng thêm Javascript bằng cách thêm vào một số static types.

Types cung cấp một phương thức tường minh hơn để mô tả các hình thái của object, mô tả documentation tốt hơn, thông qua đó TypeScript có thể xác định rằng code của chúng ta đang hoạt động chính xác hay không. Trong bài viết này chúng ta cùng nhau tìm hiểu về Generics trong TypeScript.

image.png

Generics là gì?

Generics là một tính năng trong TypeScript và các ngôn ngữ lập trình khác, cho phép chúng ta viết một function, class hay interface chung cho nhiều loại dữ liệu khác nhau, và chỉ xác định loại dữ liệu cụ thể khi sử dụng loại dữ liệu đó.

Cú pháp Generics

Generics trong TypeScript bên trong dấu ngoặc nhọn, ở định dạng <T>, trong đó T đại diện cho type được truyền vào. <T> có thể được đọc dưới dạng chung của type T.

Trong trường hợp này, T sẽ hoạt động giống như cách các tham số hoạt động trong các hàm, dưới dạng placeholders cho một type sẽ được khai báo khi một instance của cấu trúc được tạo. Do đó, generics type được chỉ định bên trong dấu ngoặc nhọn còn được gọi là generic type parameters hoặc chỉ type parameters. Multiple generic types cũng có thể xuất hiện trong một định nghĩa, chẳng hạn như <T, K, A>.

Ví dụ:

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

const output1 = identity<string>("myString");  // type of output will be 'string'
console.log(output1);

const output2 = identity<number>(123);  // type of output will be 'number'
console.log(output2);

Tạo mới instance generics class

Để tạo một instance của một generic class, chúng ta cần dựng class và báo cho compiler biết thông qua cú pháp <>. Chúng ta có thể sử dụng bất kỳ type nào cho type T trong generic syntax, bao gồm base types, classes hoặc interface.

Ví dụ:

class GenericClass<T> {
  field: T;

  constructor(field: T) {
    this.field = field;
  }
}

const instance1 = new GenericClass<number>(123);
console.log(instance1.field); // Output: 123, type of output will be 'number'

Trong ví dụ trêm. chúng ta đã tạo một generic class với một tham số kiểu dữ liệu T. Khi tạo một thể hiện của class này, chúng ta cần chỉ định kiểu dữ liệu cụ thể mà chúng ta muốn sử dụng (trong trường hợp này là number)

Hạn chế type T

Khi sử dụng generics, đôi khi nên hạn chế type T chỉ là một type cụ thể, hoặc tập hợp con của các types. Trong những trường hợp này, chúng ta không muốn code generic của mình có sẵn cho bất kỳ type của đối tượng nào, chúng ta chỉ muốn nó cho một tập hợp con cụ thể của các đối tượng. TypeScript sử dụng tính kế thừa để thực hiện điều này với generics.

Ví dụ 1:

class Shape {
  width: number;
  height: number;
}

class GenericClass<T extends Shape> {
  field: T;

  setField(field: T): void {
    this.field = field;
  }

  getField(): T {
    return this.field;
  }
}

const instance = new GenericClass<Shape>();
instance.setField({width: 100, height: 200});
console.log(instance.getField()); // Output: { width: 100, height: 200 }

Trong ví dụ trên, chúng ta đã tạo một generic class GenericClass với kiểu T thừa kế từ Shape class. Điều này có nghĩa là chỉ các instance của Shape class hoặc lớp kế thừa từ Shape mới được phép truyền vào generic class này

Ví dụ 2:

class GenericClass<T extends number | string> {
  field: T;

  setField(field: T): void {
    this.field = field;
  }

  getField(): T {
    return this.field;
  }
}

const instance = new GenericClass<string>();
instance.setField("Hello World");
console.log(instance.getField()); // Output: "Hello World"

Ở ví dụ 2 trên, chúng ta cũng sử dụng extends để chỉ định T chỉ có thể là number hoặc string. Điều này có nghĩa là khi tạo một thể hiện của class, chúng ta chỉ có thể chỉ định kiểu dữ liệu là number hoặc string.

Generic interfaces

Chúng ta cũng có thể sử dụng interfaces với generic type syntax.

ví dụ:

interface GenericInterface<T> {
  field: T;
  setField(field: T): void;
  getField(): T;
}

class ImplementationClass implements GenericInterface<number> {
  field: number;

  setField(field: number): void {
    this.field = field;
  }

  getField(): number {
    return this.field;
  }
}

const instance = new ImplementationClass();
instance.setField(123);
console.log(instance.getField()); // Output: 123

Trong ví dụ này thì chúng ta đã tạo một interface chung là GenericInterface với type T. Sau đó tạo một class ImplementationClass implements interface GenericInterface và chỉ định string là type T. Khi sử dụng class ImplementationClass, chúng ta phải tuân theo các yêu cầu của generic interface, bao gồm thuộc tính, phương thức và kiểu dữ liệu cụ thể.

Tạo các đối tượng mới trong generics

Đôi khi, các generic classes có thể cần tạo một object thuộc loại được truyền vào như là type T.

Ví dụ:

class FirstClass {
    id: number = 10;
}
class SecondClass {
    name: string = 'my name';
}
class GenericCreator< T > {
    create(): T {
        return new T();
    }
}

var creator1 = new GenericCreator<FirstClass>();
var firstClass: FirstClass = creator1.create();
var creator2 = new GenericCreator<SecondClass>();
var secondClass : SecondClass = creator2.create();

Ở ví dụ trên, chúng ta định nghĩa 2 class: FirstClass, SecondClass. Sau đó chúng ta có một generic class và một function create. Create function này để tạo một instance mới của type T. Ở 4 dòng cuối của ví dụ chúng ta muốn sử dụng GenericCreator class để tạo instance mới.

Khi chạy ở ví dụ trên chúng ta sẽ tạo ra lỗi biên dịch TypeScript

Theo tài liệu TypeScript, để cho phép một generic class tạo các đối tượng thuộc type T, chúng ta cần tham chiếu đến type T bằng hàm khởi tạo của nó. Nên trong ví dụ trên create function cần được viết lại như sau:

class GenericCreator< T > {
    create(c: { new(): T }) : T {
        return new c();
    }
}

Chúng ta sẽ chia create function thành các phần cấu thành nó. Đầu tiên là một đối số được truyền vào, tên là c. Đối số này được định nghĩa là thuộc type {new(): T}. Đây là cách cho phép chúng ta tham chiếu đến T bằng hàm khởi tạo của nó. Sau đó, chúng ta định nghĩa một anonymous type mới overloads new() function để có một constructor trả về type T. Mục đích của function này đơn giản là trả về một instance mới của biến c.

Sau khi viết lại create function giúp chúng bỏ lỗi biên dịch trước. Tuy nhiên, thay đổi này chúng ta phải truyền class definition tới create function, như sau:

var creator1 = new GenericCreator<FirstClass>();
var firstClass: FirstClass = creator1.create(FirstClass);
console.log(firstClass.id) // output: 10

var creator2 = new GenericCreator<SecondClass>();
var secondClass : SecondClass = creator2.create(SecondClass);
console.log(secondClass.name) // output: "my name"

any và generics type

Cả anygenerics đều là cách để chỉ định kiểu dữ liệu trong TypeScript. Tuy nhiên, hai cách này có một số khác biệt quan trọng.

  • any: any là một kiểu chung, nghĩa là bất kỳ giá trị nào cũng có thể được gán cho một biến hoặc tham số có kiểu any. Điều này làm cho chúng ta không có sự kiểm soát về kiểu dữ liệu hoặc tính toàn vẹn của dữ liệu, vì vậy việc sử dụng any có thể dẫn đến một số lỗi trong quá trình phát triển.
  • generics: Generics là một cách để chỉ định một kiểu chung cho một class, interface hoặc function. Generics type cho phép chúng ta chỉ định đối tượng cụ thể cần phải có một tập hợp các thuộc tính hoặc phương thức và hạn chế kiểu dữ liệu mà chúng ta muốn sử dụng. Điều này cho phép chúng ta có sự kiểm soát về kiểu dữ liệu và tính toàn vẹn của dữ liệu, giúp tránh lỗi trong quá trình phát triển.

Vì vậy, nếu muốn kiểm soát kiểu dữ liệu và giảm tình trạng lỗi trong code, chúng ta nên sử dụng generics thay vì any.

Kết luận

Trong bài viết này chúng ta cùng nhau tìm hiểu về Generics type trong TypeScript. Trong bài viết sau chúng ta cùng nhau tìm hiểu về mixins pattern sử dụng generics type với tính kế thừa. Cảm ơn các bạn đã theo dõi bài viết ❤️

Tài liệu tham khảo


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí