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.
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ả any
và generics
đề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ểuany
. Đ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ụngany
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
- Mastering TypeScript - Second Edition
- https://www.typescriptlang.org/
- ChatGPT
All rights reserved