0

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

Trong bài viết này mình và các bạn sẽ cùng tìm hiểu về Generic type , một khái niệm khá quen thuộc và quan trọng trong Typescript .

Vậy Generic là gì

Đối với Typescript , Generics được định nghĩa như là một công cụ cho phép bạn tạo ra các đoạn code để có thể sử dụng lại với nhiều type khác nhau một cách linh hoạt thay vì duy nhất một type . Đồng thời nó giúp bạn tránh trùng lặp các đoạn code có chức năng tương tự mà vẫn thể hiện rõ mục đích sử dụng

Để hiểu rõ hơn , mình sẽ giải thích thông qua một ví dụ đơn giản sau. Bài toán đó là bạn sẽ cần phải lấy ra phần tử đầu tiên của một mảng string

function getFirstElement(arr:string[]): string {
        return arr[0]
}

Muốn lấy phần tử đầu tiên của mảng chúng ta chỉ cần sử dụng chỉ mục (index) là 0 tương ứng với element đứng đầu tiên.Và đây là cách sử dụng hàm trên

console.log(getFirstElement(['anh','dep','zai'])) //result-> anh

Tuy nhiên hàm getFirstElement có một nhược điểm đó là hàm đó chỉ sử dụng được với các tham số đầu vào là một mảng string vậy nếu tham số arr là một mảng các number , hay một mảng các object chẳng hạn bạn sẽ lại phải định nghĩa thêm 1 hàm tương tự trong khi logic vẫn giống như hàm ban đầu

function getFirstElementNumber(arr:number[]): number {
        return arr[0]
}
console.log(getFirstElementNumber([1,2,3])) //result-> 1

Vậy có cách nào khác để sử tạo ra 1 function mà dùng chung cho tất cả mọi type ? Chắc hẳn nhiều bạn sẽ nghĩ đến việc dùng đến any , type cho mọi thứ đều ok .

function getFirstElement(arr:any[]): any {
        return arr[0]
}
console.log(getFirstElement(['anh','dep','zai'])) //result-> anh
console.log(getFirstElement([1,2,3])) //result-> 1

Khi bạn chạy đoạn code trên bạn sẽ thấy mọi thứ hoạt động bình thường . Tuy nhiên nó có một nhược điểm lớn đó là bạn sẽ không biết type của phần tử được trả về , hay nói cách khác any là kiểu không an toàn .

Generic trong Typescript

Để giải quyết vấn đề trên , Generic đã được sinh ra và được coi là 1 giải pháp an toàn để tránh tình trạng các đoạn code bị lặp lại . Bạn sẽ tạo một hàm sử dụng Generic như sau :

function getFirstElement<T>(arr:T[]): T {
        return arr[0]
}

Hàm trên sẽ trả về phần tử đầu tiên của mảng có kiểu T và sử dụng kiểu T cho kết quả được trả về . Hay nói cách khác khi bạn truyền một array với một kiểu nào đó chẳng hạn như string , trình biên dịch sẽ tự động xác định kiểu dữ liệu T chính là string

let arrString = [‘leuleu’,’huhu’,’haha’]
console.log(getFirstElement(arrString)) //result->  ‘leuleu’

Sử dụng nhiều kiểu dữ liệu trong hàm generic

Chúng ta có ví dụ như sau

function merge<U,V>(object1:U,object2:V){
    return {
        ...object1,
        ...object2
    }
}

let time = merge(
    {date:22},
    {hour:13}
)
console.log(time) // resulst {date:22,hour:13}

Ràng buộc generic type trong Typescript (Generic Constraints)

Với ví dụ sử dụng nhiều kiểu dữ liệu bên trên chúng ta có thể merge nhiều các đối tượng lại với nhau và chúng hoạt động bình thường . Tuy nhiên nếu một trong trong 2 kiểu U, V không phải là object thì sao . Hãy xem ví dụ sau

let info = merge(
    {
        name: 'john',
        age:22
    },
    1997
)
console.log(info)//{ name: 'john', age: 22 }

Nó vẫn sẽ hoạt động bình thường và ko có bất kì điều gì xảy ra. Điều đó chúng tỏ rằng việc bạn cho truyền tham số một cách ngầu nhiên như vậy là không an toàn khi hàm merge hoạt động với mọi loại dữ liệu . Để giải quyết vấn đề này chúng ra sẽ phải tạo ra các ràng buộc cho dữ liệu để hàm merge chỉ có thể hoạt động với các object .

function merge<U extends object,V extends object >(ob1:U,ob2:V):(U&V){
    return {
        ...ob1,
        ...ob2
    }
}

let resutl = merge(
    {name:'john'},
    {age:2222}
)
console.log(resutl)/////{ name: 'john', age: 22222 }

let resutl2 = merge(
    {name:'john'},
    222
)

console.log(resutl2)/////{ name: 'john', age: 22222 }
ERROR: Argument of type 'number' is not assignable to parameter of type 'object'.

Tử khoá extends sẽ được sử dụng để ràng buộc các kiểu dữ liệu cho generic type , và nó yêu cầu tất cả các tham số sẽ đều phải là object

Sử dụng Generic với class

Chúng ta sẽ tạo ra 1 class generic với kiểu dữ liệu có cấu trúc Stack (LIFO) . Mỗi stack sẽ có một kích thước nhất định và có 2 phương thức chính là Push ( thêm phần tử ) và Pop (lấy phần tử)

class Stack<T>{
    elements : T[] = []
    size : number
   constructor(size:number){
       this.size = size
   }

   push(element:T){
       if(this.elements.length == this.size){
           throw new Error('stack is overflow')
       }
       this.elements.push(element)
   }

   pop():T{
       if(this.elements.length ==0){
           throw new Error('stack is empty')
       }
       return this.elements.pop()
   }
}
let numbers = new Stack<number>(5)
numbers.push(1)
numbers.push(2)
numbers.push(3)
console.log(numbers)//Stack { elements: [ 1, 2, 3 ], size: 5 }
numbers.pop()
console.log(numbers) // Stack { elements: [ 1, 2 ], size: 5 }

Sử dụng generic với Interface

Generic Interface có thể được tạo với 1 hoặc nhiểu kiểu dữ liệu để mô tả các thuộc tính của đối tượng Ví dụ như

interface People<U,V>{
    name:U,
    age:V
} 

let student : People<string,number> = {
    name:'john',
    age:22
}
let teacher : People<string,number> = {
    name:'sira',
    age:42
}

Generic Interface mô tả các phương thức

interface Collection<T> {
    add(o: T): void;
    remove(o: T): void;
}

class List<T> implements Collection<T>{
    private items: T[] = [];

    add(o: T): void {
        this.items.push(o);
    }
    remove(o: T): void {
        let index = this.items.indexOf(o);
        if (index > -1) {
            this.items.splice(index, 1);
        }
    }
}

Class List<T> sẽ phải tuân thủ interface Collection<T> và bắt buộc có các method add và remove như trên

Tổng kết

Qua bài biết này mình đã giới thiệu sơ qua về generic trong typescript , mình hi vọng sẽ giúp các bạn hiểu hơn về typescript trong các bài viết tới !


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í