Tìm hiểu về Generic type trong TypeScript

Tram Ho

Trong bài viết này, chúng ta sẽ đi sâu tìm hiểu về Generics( Generic data types) trong Typescript. Đối với các bạn dev C# hoặc Java thì Generic data type vô cùng quen thuộc, tuy nhiên đối với các bạn dev javascript khi mới chuyển qua Typescpirt thì sẽ gặp đôi chút khó khăn khi tiếp cận với khái niệm này

1. Giới thiệu

Đầu tiên, chúng ta hãy cùng tìm hiểu Generic type là gì ?
Theo như định nghĩa của TypeScript về Generic:

In languages like C# and Java, one of the main tools in the toolbox for creating reusable components is generics, that is, being able to create a component that can work over a variety of types rather than a single one. This allows users to consume these components and use their own types.

Hiểu đơn giản thì Generic type là việc cho phép truyền type vào components(function, class, interface) như là 1 tham số.
Điều này sẽ giúp các components mềm dẻo hơn. Tái sử dụng tốt hơn.

2. Tại sao lại cần Generic

Chúng ta tạo một function nhận vào 2 tham số cùng kiểu dữ liệu (string | number) và return về một Turple.

Ở ví dụ trên function getTuple có 2 tham số ab có kiểu NS ( Union type) và trả về một tuple [NS, NS].

Bây giờ chúng ta có một vài vấn đề với function trên:

  • Đầu tiên chúng ta không thể ràng buộc ab có cùng kiểu dữ liệu bời vì ab có thể là chuỗi hoặc số.
  • Thứ hai là khi fuction trả về 1 tuple (array) chứa các giá trị có kiểu string hoặc number và trình biên dịch Typescript không cho phép ta làm như vậy bởi vì nó cần phải biết chính xác kiểu dữ liệu của các giá trị trả về.

Cách để giải quyết vấn đề là sử dụng any type cho ab và tuple [any, any]. Hoặc ta có thể sử dụng type assertion để ép kiểu giá trị trong tuple (NS thành string hoặc number).
Tuy nhiên cả 2 cách đều có thể gây ra lỗi nếu như chúng ta không tiến hành kiểm tra thủ công kiểu dữ liệu của các giá trị.

Và Generic type xuất hiện, giúp chúng ta giải quyết những vấn đề trên

Typescript hỗ trợ mạnh cho generic, chúng ta có thể sử dụng generic cho function, class, interface….
Bây giờ ta sẽ sửa lại ví dụ trên bằng cách sử dụng Generic function:

Ta có thể ràng buộc a và b cùng kiểu dữ liệu bằng cách dùng generic type.
Khi ta sử dụng cú phápgetTuple<number>, trình biên dịch Typescrpit sẽ thay thể T thành number. Vì vậy trình biên dịch TS sẽ diễn giải hàm getTuple như dưới đây :

Dó đó giá trị trả về của hàm là tuple [number, number] và trình biên dịch TS sẽ cho ta thao tác lên bộ giá trị này.(tương tự với string)
Chúng ta có thể thay T bằng bất cứ tham số nào. Cú pháp f<Type>().

Ở ví dụ trên, ta để ý , khi ta gọi làm getTuple( 1.25, 'world' ).Trong lệnh gọi hàm, chúng ta đã bỏ giá trị của tham số generic. Vì vậy, TypeScript sẽ suy ra kiểu dữ liệu cho T từ kiểu của đối số đầu tiên1.25 là number . Do đó đối số thứ hai phải có cùng kiểu, nên lệnh gọi này sẽ dẫn đến lỗi biên dịch vì cả hai đối số phải cùng kiểu với mỗi khai báo hàm.Tương tự nếu ta thay đối số đầu tiên là ‘word’ thì trình biên dịch TS sẽ suy ra kiểu dữ liệu của Tstring.

Ta có thể viết lại hàm getTuple bằng arrow function generic như sau :

3. Khai báo Generic type

Chúng ta có thể sử dụng generic cho function, class, interface….

3.1 Generic Function

Như chúng ta đã biết, TypeScript có thể suy ra kiểu dữ liệu từ giá trị. Nếu bạn di chuột vào tên hàm getTuple trong IDE của mình, bạn sẽ thấy kiểu dữ liệu trả về bên dưới của hàm. Đây là kiểu của hàm chúng ta vừa tạo.

Một generic type có thể chứa nhiều tham số khác nhau đại diện cho nhiều kiểu dữ liệu khác nhau, ví dụ nếu 2 tham số ab khác nhau thì ta phải cần 2 tham số khác nhau để đại diện kiểu dữ liệu riêng cho chúng.

Do đó khi ta gọi hàm getTuple( 1.25, 'world' ) thì trình biên dịch sẽ không báo lỗi nữa vì đối số ab có thể có các kiểu riêng biệt.
Cách khai báo hàm getTuple tương tự :

3.2 Generic Interface

Một Interface cũng có thể đại diện cho một function.

Interface trên sẽ đại diện cho một function nhận vào 2 đối số là numberstring, trả về giá trị có kiểu là any.Ta có thể sử dụng interface này để khai báo type cho function:

Đối số a ,b sẽ nhận kiểu numberstring. Giá trị trả về sẽ có kiểu any.

3.2.1 Khai báo generic function sử dụng interface

Ví dụ này giống với ví dụ trước đó, chỉ khác chúng ta tạo một interface kèm một generic function. Sẽ giúp chúng ta linh động hơn với kiểu dữ liệu của ab.

3.2.2 Định nghĩa một generic interface

Interface cho phép chúng ta định nghĩa các thuộc tính và phương thức của 1 object. Hãy tưởng tượng một object có các thuộc tính ab và có hàm getTuple trả về tuple có kiểu dữ liệu của ab.Làm thế nào để viết interface đó?

Có thể bạn sẽ nghĩ như trên, tuy nhiên cách này sẽ không đúng. ERROR : cannot find name 'T'.
Cú pháp đúng sẽ là :

Ex:

Hãy xem xét một tình huống khác cho ví dụ trên. Điều gì sẽ xảy ra nếu hàm getTuple chấp nhận một generic argument?

Lúc này type của c là cố định theo V.
Trong một vài trường hợp chúng ta muốn type của c linh động hơn mà không phụ thuộc vào generic của TupleObject
ta có thể làm:

3.3 Generic class

Ex :

Trong ví dụ trên Collection là 1 generic class, khi tạo 1 thực thể của class Collection bằng từ khóa new ta cung cấp type T bằng syntax new Collection<Type>. Trình biên dịch TS sẽ thay thế T bằng type mà ta cung cấp.
Type T được sử dụng (suy ra) trong các thuộc tính public, private,protected, trong các phương thức cũng như là contructor.
Tuy nhiên đối với thuộc tính hoặc phương thứcstatic thì trình biên dịch TS không cho sử dụng generic T.

Bạn có thể chuyển generic type từ lớp con sang lớp cha trong kế thừa được minh họa bên dưới. Mình đã sử dụng tham số U chỉ để chứng minh rằng tên tham số không quan trọng giữa các lớp và điều tương tự cũng áp dụng cho Interface.

4.Ràng buộc generic type

4.1 Ràng buộc bằng extends

Ex :

Hàm merge là một hàm hợp nhất 2 đối tượng, Ex:

Nó hoạt động tốt, hàm merge() muốn bạn truyền vào 2 object, tuy nhiên nó không ngăn bạn truyền vào như thế này :

TS không gặp bất cứ lỗi nào, hàm merge() làm việc với tất cả dữ liệu, tuy nhiên ta có thể ràng buộc hàm merge hoạt động với các object. Để làm được điều đó, ta sử dụng từ khóa extends:

Bây giờ hàm merge() đã bị ràng buộc kiểu dữ liệu.

4.2 Ràng buộc bằng extends keyof

Ex:

Hàm props nhận vào 2 tham số là một đối tượng và 1 key của đối tượng.
Trình biên dịch sẽ gặp lỗi sau:

Để khắc phục bạn phải ràng buộc kiểu dữ liệu K là key của kiểu dữ liệu T .

Nếu bạn truyền vào prop() một key hợp lệ thì trình biên dịch sẽ không báo lỗi :

Tóm lược

  • Sử dụng generic type trong typescript để tạo các funtion , interface, class… có tính linh động, tái sử dụng cao.
  • Sử dụng từ khoá extends để giới hạn kiểu dữ liệu của tham số thành kiểu dữ liệu cụ thể.
  • Sử dụng từ khoá extends of để ràng buộc kiểu dữ liệu là thuộc tính của một đối tượng khác.

Tài liệu tham khảo

https://www.typescriptlang.org/docs/handbook/generics.html
https://www.tutorialsteacher.com/typescript/typescript-generic
https://medium.com/jspoint/typescript-generics-10e99078cc8

Chia sẻ bài viết ngay

Nguồn bài viết : Viblo