Generic type trong TypeScript

Generic type trong TypeScript

Trong hướng dẫn này, bạn sẽ tìm hiểu về generic type trong TypeScript cho phép bạn sử dụng các kiểu dữ liệu làm tham số chính thức.

Giới thiệu về generic type trongTypeScript

Generic type trong TypeScript cho phép bạn viết các function, class và interface có thể tái sử dụng và tổng quát hóa. Trong hướng dẫn này, bạn sẽ tập trung vào việc tạo các hàm generic (generic function).

Sẽ dễ dàng hơn để giải thích generic type trong TypeScript thông qua một ví dụ đơn giản.

Giả sử bạn cần tạo một function trả về một phần tử ngẫu nhiên trong một mảng số.

Hàm sau getRandomNumberElement() nhận một mảng số làm tham sốvà trả về một phần tử ngẫu nhiên từ mảng:

function getRandomNumberElement(items: number[]): number {
    let randomIndex = Math.floor(Math.random() * items.length);
    return items[randomIndex];
}

Để nhận một phần tử ngẫu nhiên của một mảng, bạn cần:

  • Tìm chỉ số ngẫu nhiên trước.
  • Nhận phần tử ngẫu nhiên dựa trên chỉ số ngẫu nhiên.

Để tìm chỉ số ngẫu nhiên của một mảng, chúng tôi sử dụng hàm Math.random() trả về một số ngẫu nhiên từ 0 đến 1, nhân nó với độ dài của mảng và áp dụng hàm Math.floor() để làm tròn xuống cho kết quả.

Sau đây là cách sử dụng hàm getRandomNumberElement():

let numbers = [1, 5, 7, 4, 2, 9];
console.log(getRandomNumberElement(numbers));

Giả sử rằng bạn cần lấy một phần tử ngẫu nhiên từ một mảng kiểu chuỗi. Lần này, bạn tạo một hàm mới:

function getRandomStringElement(items: string[]): string {
    let randomIndex = Math.floor(Math.random() * items.length);
    return items[randomIndex];
}

Logic của hàm getRandomStringElement() cũng giống như logic trong hàm getRandomNumberElement().

Ví dụ này cho thấy cách sử dụng hàm getRandomStringElement():

let colors = ['red', 'green', 'blue'];
console.log(getRandomStringElement(colors));

Sau đó, bạn có thể cần lấy một phần tử ngẫu nhiên trong một mảng đối tượng. Làm theo cách trên thì bạn sẽ phải tạo một hàm mới mỗi khi bạn muốn lấy một phần tử ngẫu nhiên từ một kiểu mảng mới là không thể tái sử dụng các hàm có sẵn.

Sử dụng kiểu any

Một giải pháp cho vấn đề này là thiết lập kiểu của mảng là any[]. Bằng cách này, bạn chỉ cần viết một hàm hoạt động với một mảng thuộc bất kỳ kiểu nào.

function getRandomAnyElement(items: any[]): any {
    let randomIndex = Math.floor(Math.random() * items.length);
    return items[randomIndex];
}

Hàm getRandomAnyElement() nhận đầu vào là một mảng kiểu any. Do đó, nó có thể là một mảng số, chuỗi, đối tượng, v.v.:

let numbers = [1, 5, 7, 4, 2, 9];
let colors = ['red', 'green', 'blue'];

console.log(getRandomAnyElement(numbers));
console.log(getRandomAnyElement(colors));

Giải pháp này hoạt động tốt. Tuy nhiên, nó có một nhược điểm.

Nó không cho phép bạn thực thi kiểu của phần tử trả về. Nói cách khác, nó không an toàn kiểu.

Một giải pháp tốt hơn để tránh trùng lặp mã trong khi vẫn an toàn kiểu là sử dụng generic type.

Generic type trong TypeScript

Phần sau cho thấy một hàm generic trả về phần tử ngẫu nhiên từ một mảng kiểu T:

function getRandomElement<T>(items: T[]): T {
    let randomIndex = Math.floor(Math.random() * items.length);
    return items[randomIndex];
}

Hàm này sử dụng biến kiểu T. Kiểu T cho phép bạn chỉ định kiểu dữ liệu tại thời điểm gọi hàm. Ngoài ra, hàm sử dụng biến kiểu T làm kiểu trả về của nó.

Hàm getRandomElement() là hàm generic bởi vì nó có thể làm việc với bất kỳ kiểu dữ liệu nào, ví dụ: chuỗi, số, đối tượng, …

Theo quy ước, chúng ta sử dụng ký tự T làm kiểu dữ liệu của biến. Tuy nhiên, bạn có thể sử dụng các chữ khác như A, BC, …

Gọi hàm generic trong TypeScript

Sau đây là cách sử dụng hàm getRandomElement() với một mảng số:

let numbers = [1, 5, 7, 4, 2, 9];
let randomEle = getRandomElement<number>(numbers); 
console.log(randomEle);

Ví dụ này chỉ định rõ kiểu number cho T khi gọi hàm getRandomElement().

Trong thực tế, bạn sẽ sử dụng kiểu suy luận cho đối số. Có nghĩa là bạn để trình biên dịch TypeScript tự xác định kiểu dữ liệu của T dựa trên loại đối số mà bạn truyền vào, như thế này:

let numbers = [1, 5, 7, 4, 2, 9];
let randomEle = getRandomElement(numbers); 
console.log(randomEle);

Trong ví dụ này, chúng tôi đã không chỉ định kiểu number một cách rõ ràng khi gọi hàm getRandomElement(). Trình biên dịch sẽ xác định kiểu dữ liệu của T dựa vào kiểu dữ liệu của tham số truyền vào.

Bây giờ, hàm getRandomElement() cũng là kiểu an toàn. Ví dụ: nếu bạn gán giá trị trả về cho một biến kiểu chuỗi, bạn sẽ gặp lỗi:

let numbers = [1, 5, 7, 4, 2, 9];
let returnElem: string;
returnElem = getRandomElement(numbers);  // compiler error

Hàm generic có nhiều kiểu dữ liệu trong TypeScript

Phần sau minh họa cách tạo một hàm generic với hai biến kiểu UV:

function merge<U, V>(obj1: U, obj2: V) {
    return {
        ...obj1,
        ...obj2
    };
}

Hàm merge() kết hợp hai đối tượng với kiểu UV. Nó kết hợp các thuộc tính của hai đối tượng thành một đối tượng duy nhất.

Kiểu suy luận suy ra giá trị trả về của hàm merge() là kiểu giao của UV, là U & V.

Sau đây minh họa cách sử dụng hàm merge() để hợp nhất hai đối tượng:

let result = merge(
    { name: 'John' },
    { jobTitle: 'Frontend Developer' }
);

console.log(result);

Đầu ra:

{ name: 'John', jobTitle: 'Frontend Developer' }

Lợi ích của generic type trong TypeScript

Sau đây là những lợi ích của generic type trong TypeScript:

  • Kiểm tra kiểu dữ liệu tại thời điểm biên dịch.
  • Loại bỏ ép kiểu.
  • Cho phép bạn triển khai các thuật toán generic.

Ràng buộc generic type trong TypeScript

Trong phần này, bạn sẽ tìm hiểu về ràng buộc generic type trong TypeScript.

Sử dụng ràng buộc generic type trong TypeScript

Hãy xem ví dụ sau:

function merge<U, V>(obj1: U, obj2: V) {
    return {
        ...obj1,
        ...obj2
    };
}

Hàm merge() là một hàm generic dùng để hợp nhất hai đối tượng. Ví dụ:

let person = merge(
    { name: 'John' },
    { age: 25 }
);

console.log(result);

Đầu ra:

{ name: 'John', age: 25 }

Nó hoạt động hoàn toàn tốt.

Hàm merge() mong đợi đầu vào là hai đối tượng. Tuy nhiên, nó không ngăn bạn truyền vào tham số như thế này:

let person = merge(
    { name: 'John' },
    25
);

console.log(person);

Đầu ra:

{ name: 'John' }

TypeScript không gặp bất kỳ lỗi nào.

Thay vì cho phép hàm merge() làm việc với tất cả các kiểu dữ liệu, bạn có thể ràng buộc hàm merge() để nó chỉ hoạt động với các đối tượng.

Để làm điều này, bạn cần tạo rạng buộc cho kiểu dữ liệu của UV.

Để ràng buộc kiểu dữ liệu cho generic type, bạn sử dụng từ khóa extends. Ví dụ:

function merge<U extends object, V extends object>(obj1: U, obj2: V) {
    return {
        ...obj1,
        ...obj2
    };
}

Bây giờ hàm merge() đã bị ràng buộc kiểu dữ liệu nên nó sẽ không còn hoạt động với tất cả các kiểu dữ liệu. Thay vào đó, nó chỉ hoạt động với kiểu object.

Điều sau sẽ dẫn đến lỗi:

let person = merge(
    { name: 'John' },
    25
);

Lỗi:

Argument of type '25' is not assignable to parameter of type 'object'.

Sử dụng ràng buộc generic type cho các tham số phụ thuộc

TypeScript cho phép bạn khai báo một tham số có kiểu dữ liệu bị ràng buộc bởi kiểu dữ liệu của một tham số khác.

Hàm prop() sau chấp nhận hai tham số là một đối tượng và một tên thuộc tính. Nó trả về giá trị của thuộc tính.

function prop<T, K>(obj: T, key: K) {
    return obj[key];
}

Trình biên dịch gặp lỗi sau:

Type 'K' cannot be used to index type 'T'.

Để khắc phục lỗi này, bạn thêm ràng buộc cho kiểu dữ liệu K để đảm bảo rằng nó là một khóa của kiểu dữ liệu T như sau:

function prop<T, K extends keyof T>(obj: T, key: K) {
    return obj[key];
}

Nếu bạn truyền vào hàm prop một tên thuộc tính tồn tại trên obj, trình biên dịch sẽ không báo lỗi. Ví dụ:

let str = prop({ name: 'John' }, 'name');
console.log(str);

Đầu ra:

John

Tuy nhiên, nếu bạn truyền một khóa không tồn tại trên đối số đầu tiên, trình biên dịch sẽ phát ra lỗi:

let str = prop({ name: 'John' }, 'age');

Lỗi:

Argument of type '"age"' is not assignable to parameter of type '"name"'.

Generic class trong TypeScript

Trong phần này, bạn sẽ tìm hiểu cách tạo các lớp generic (generic class) trong TypeScript.

Giới thiệu về generic class trong TypeScript

Lớp generic có danh sách tham số kiểu generic trong dấu <> theo sau tên lớp:

class className<T>{
    //... 
}

TypeScript cho phép bạn có nhiều kiểu generic trong danh sách tham số. Ví dụ:

class className<K,T>{
    //...
}

Các ràng buộc cũng được áp dụng cho các kiểu generic trong lớp:

class className<T extends TypeA>{
    //...
}

Ví dụ về generic class trong TypeScript

Trong ví dụ này, chúng ta sẽ tạo lớp Stack.

Ngăn xếp (stack) là một cấu trúc dữ liệu hoạt động theo nguyên tắc vào sau ra trước (hay LIFO). Nó có nghĩa là phần tử đầu tiên bạn đặt vào ngăn xếp là phần tử cuối cùng bạn có thể nhận được từ ngăn xếp.

Thông thường, một ngăn xếp có một kích thước. Theo mặc định, nó trống. Ngăn xếp có hai hoạt động chính:

  • Push: đưa một phần tử vào ngăn xếp.
  • Pop: lấy một phần tử từ ngăn xếp.

Sau đây là lớp Stack hoàn chỉnh:

class Stack<T> {
    private elements: T[] = [];

    constructor(private size: number) {
    }
    isEmpty(): boolean {
        return this.elements.length === 0;
    }
    isFull(): boolean {
        return this.elements.length === this.size;
    }
    push(element: T): void {
        if (this.elements.length === this.size) {
            throw new Error('The stack is overflow!');
        }
        this.elements.push(element);

    }
    pop(): T {
        if (this.elements.length == 0) {
            throw new Error('The stack is empty!');
        }
        return this.elements.pop();
    }
}

Đoạn code sau đây tạo ra một ngăn xếp kiểu số:

let numbers = new Stack<number>(5);

Hàm này trả về một số ngẫu nhiên giữa hai số lowhigh:

function randBetween(low: number, high: number): number {
    return Math.floor(Math.random() * (high - low + 1) + low);
}

Bây giờ, bạn có thể sử dụng hàm randBetween() để tạo các số ngẫu nhiên để đưa vào ngăn xếp numbers:

let numbers = new Stack<number>(5);

while (!numbers.isFull()) {
    let n = randBetween(1, 10);
    console.log(`Push ${n} into the stack.`)
    numbers.push(n);
}

Đầu ra:

Push 3 into the stack.
Push 2 into the stack. 
Push 1 into the stack. 
Push 8 into the stack. 
Push 9 into the stack.

Phần sau cho thấy cách lấy các phần tử từ ngăn xếp cho đến khi nó trống:

while (!numbers.isEmpty()) {
    let n = numbers.pop();
    console.log(`Pop ${n} from the stack.`);
}

Đầu ra:

Pop 9 from the stack.
Pop 8 from the stack.
Pop 1 from the stack.
Pop 2 from the stack.
Pop 3 from the stack.

Tương tự, bạn có thể tạo một ngăn xếp kiểu chuỗi. Ví dụ:

let words = 'The quick brown fox jumps over the lazy dog'.split(' ');

let wordStack = new Stack<string>(words.length);

// push words into the stack
words.forEach(word => wordStack.push(word));

// pop words from the stack
while (!wordStack.isEmpty()) {
    console.log(wordStack.pop());
}

Nó hoạt động như thế nào:

  • Đầu tiên, tách các từ trong câu thành một mảng các từ.
  • Thứ hai, tạo một ngăn xếp có kích thước bằng số từ trong mảng từ.
  • Thứ ba, đưa các phần tử của mảng từ vào ngăn xếp.
  • Cuối cùng, lấy các từ trong ngăn xếp cho đến khi nó trống.

Generic interface trong TypeScript

Trong phần này, bạn sẽ tìm hiểu về generic interface trong TypeScript.

Giới thiệu về generic interface trong TypeScript

Giống như class, interface cũng có generic interface. Generic interface có danh sách tham số kiểu generic:

interface interfaceName<T> {
    // ...
}

Điều này làm cho phép sử dụng tham số kiểu T cho tất cả các thành viên của interface.

Generic interface có thể có một hoặc nhiều kiểu generic. Ví dụ:

interface interfaceName<U,V> {
    // ...
}

Ví dụ về generic interface trong TypeScript

Hãy lấy một số ví dụ về việc khai báo các generic interface trong TypeScript.

Generic interface mô tả thuộc tính đối tượng

Sau đây trình bày cách khai báo một generic interface bao gồm hai thành viên khóa và giá trị với các kiểu tương ứng KV:

interface Pair<K, V> {
    key: K;
    value: V;
}

Bây giờ, bạn có thể sử dụng interface Pair<K, V> để định nghĩa bất kỳ cặp khóa / giá trị nào với bất kỳ kiểu dữ liệu nào. Ví dụ:

let month: Pair<string, number> = {
    key: 'Jan',
    value: 1
};

console.log(month);

Trong ví dụ này, chúng tôi khai báo biến month có kiểu dữ liệu Pair<string, number> có khóa là một chuỗi và giá trị là một số.

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

Phần sau khai báo một generic interface với hai phương thức add() và remove():

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

Và lớp List<T> triển khai interface Collection<T>:

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);
        }
    }
}

Từ lớp List<T>, bạn có thể tạo danh sách các giá trị thuộc nhiều kiểu dữ liệu khác nhau, ví dụ như số hoặc chuỗi.

Ví dụ: phần sau cho thấy cách sử dụng lớp List<T> để tạo danh sách các số:

let list = new List<number>();

for (let i = 0; i < 10; i++) {
    list.add(i);
}

Generic interface mô tả các kiểu chỉ mục

Sau đây khai báo một generic interface mô tả một kiểu chỉ mục:

interface Options<T> {
    [name: string]: T
}

let inputOptions: Options<boolean> = {
    'disabled': false,
    'visible': true
};

Tóm lược

  • Sử dụng generic type trong TypeScript để tạo các function, interface và class có thể tái sử dụng, tổng quát và an toàn cho kiểu.
  • Sử dụng từ khóa extends để giới hạn kiểu dữ liệu của tham số thành một kiểu cụ thể.
  • Sử dụng extends keyof để ràng buộc một kiểu dữ liệu là thuộc tính của đối tượng khác.

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *