MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript泛型类的设计与使用场景

2024-01-027.1k 阅读

TypeScript泛型类的设计

泛型类基础概念

在TypeScript中,泛型类允许我们在定义类的时候不指定具体的数据类型,而是在使用类的时候再确定类型。这提供了一种非常灵活的方式来创建可复用的类,使得这些类可以适应多种不同的数据类型,而不需要为每种类型都创建一个单独的类。

泛型类的语法形式如下:

class GenericClass<T> {
    private value: T;
    constructor(value: T) {
        this.value = value;
    }
    getValue(): T {
        return this.value;
    }
}

在上述代码中,<T> 是类型参数,T 在这里就像是一个占位符,代表了具体的数据类型。在 GenericClass 类中,value 属性的类型是 T,构造函数接收一个类型为 T 的参数 value,并且 getValue 方法返回的类型也是 T

泛型类设计的灵活性

泛型类的设计赋予了开发者极大的灵活性。以一个简单的栈数据结构为例,传统方式可能需要为不同数据类型分别定义栈类,如 NumberStackStringStack 等。但使用泛型类,我们可以创建一个通用的栈类:

class Stack<T> {
    private items: T[] = [];
    push(item: T) {
        this.items.push(item);
    }
    pop(): T | undefined {
        return this.items.pop();
    }
    peek(): T | undefined {
        return this.items[this.items.length - 1];
    }
    isEmpty(): boolean {
        return this.items.length === 0;
    }
}

这样,我们可以根据需要创建不同类型的栈:

let numberStack = new Stack<number>();
numberStack.push(10);
let poppedNumber = numberStack.pop();

let stringStack = new Stack<string>();
stringStack.push('hello');
let poppedString = stringStack.pop();

通过这种方式,我们仅使用一个 Stack 泛型类就满足了不同数据类型栈的需求,大大减少了代码的重复。

泛型类的继承与实现

  1. 泛型类的继承 当继承一个泛型类时,子类可以选择保留泛型参数,或者指定具体类型。
class BaseGenericClass<T> {
    value: T;
    constructor(value: T) {
        this.value = value;
    }
}

// 子类保留泛型参数
class SubGenericClass1<T> extends BaseGenericClass<T> {
    printValue() {
        console.log(this.value);
    }
}

// 子类指定具体类型
class SubGenericClass2 extends BaseGenericClass<string> {
    printLength() {
        console.log(this.value.length);
    }
}

SubGenericClass1 中,继续使用泛型参数 T,这意味着它仍然可以接受任何类型的数据。而 SubGenericClass2 则明确指定了类型为 string,这样它就可以使用 string 类型特有的属性和方法,比如 length

  1. 泛型类实现接口 泛型类同样可以实现泛型接口。
interface GenericInterface<T> {
    getValue(): T;
}

class GenericImplementation<T> implements GenericInterface<T> {
    private value: T;
    constructor(value: T) {
        this.value = value;
    }
    getValue(): T {
        return this.value;
    }
}

这里 GenericImplementation 类实现了 GenericInterface 接口,通过泛型参数 T 确保了接口方法 getValue 的返回类型与类中属性 value 的类型一致。

泛型类的约束

有时候,我们希望泛型参数满足一定的条件,这就需要对泛型进行约束。

interface Lengthwise {
    length: number;
}

class GenericWithConstraint<T extends Lengthwise> {
    private value: T;
    constructor(value: T) {
        this.value = value;
    }
    getLength(): number {
        return this.value.length;
    }
}

在上述代码中,GenericWithConstraint 类的泛型参数 T 被约束为必须实现 Lengthwise 接口,即必须有 length 属性。这样在 getLength 方法中就可以安全地访问 this.value.length

TypeScript泛型类的使用场景

数据结构相关场景

  1. 队列数据结构 队列是一种常见的数据结构,先进先出(FIFO)。使用泛型类可以创建一个通用的队列类。
class Queue<T> {
    private items: T[] = [];
    enqueue(item: T) {
        this.items.push(item);
    }
    dequeue(): T | undefined {
        return this.items.shift();
    }
    peek(): T | undefined {
        return this.items[0];
    }
    isEmpty(): boolean {
        return this.items.length === 0;
    }
}

我们可以用这个 Queue 类创建不同类型的队列,比如数字队列、字符串队列等。

let numberQueue = new Queue<number>();
numberQueue.enqueue(1);
numberQueue.enqueue(2);
let dequeuedNumber = numberQueue.dequeue();

let stringQueue = new Queue<string>();
stringQueue.enqueue('a');
stringQueue.enqueue('b');
let dequeuedString = stringQueue.dequeue();
  1. 链表数据结构 链表是一种链式存储的数据结构,每个节点包含数据和指向下一个节点的引用。
class ListNode<T> {
    value: T;
    next: ListNode<T> | null;
    constructor(value: T) {
        this.value = value;
        this.next = null;
    }
}

class LinkedList<T> {
    private head: ListNode<T> | null = null;
    add(value: T) {
        let newNode = new ListNode<T>(value);
        if (!this.head) {
            this.head = newNode;
        } else {
            let current = this.head;
            while (current.next) {
                current = current.next;
            }
            current.next = newNode;
        }
    }
    getFirst(): T | null {
        return this.head? this.head.value : null;
    }
}

通过 LinkedList 泛型类,我们可以方便地创建不同类型的链表,如整数链表、对象链表等。

集合操作场景

  1. 集合类 集合是一种不包含重复元素的数据结构。我们可以设计一个泛型集合类。
class Set<T> {
    private items: T[] = [];
    add(item: T) {
        if (!this.has(item)) {
            this.items.push(item);
        }
    }
    has(item: T): boolean {
        return this.items.includes(item);
    }
    remove(item: T) {
        this.items = this.items.filter(i => i!== item);
    }
    getItems(): T[] {
        return this.items;
    }
}

这样我们可以创建不同类型的集合,比如字符串集合、数字集合等。

let stringSet = new Set<string>();
stringSet.add('apple');
stringSet.add('banana');
let hasApple = stringSet.has('apple');

let numberSet = new Set<number>();
numberSet.add(1);
numberSet.add(2);
let hasOne = numberSet.has(1);
  1. 映射类 映射是一种键值对的数据结构。我们可以创建一个泛型映射类。
class Map<K, V> {
    private items: { [key: string]: V } = {};
    set(key: K, value: V) {
        this.items[key.toString()] = value;
    }
    get(key: K): V | undefined {
        return this.items[key.toString()];
    }
    has(key: K): boolean {
        return this.items.hasOwnProperty(key.toString());
    }
    remove(key: K) {
        if (this.has(key)) {
            delete this.items[key.toString()];
        }
    }
}

通过这个 Map 泛型类,我们可以创建不同类型键值对的映射,比如字符串到数字的映射,对象到字符串的映射等。

let stringToNumberMap = new Map<string, number>();
stringToNumberMap.set('one', 1);
let value = stringToNumberMap.get('one');

let objectToStringMap = new Map<{ name: string }, string>();
let obj = { name: 'John' };
objectToStringMap.set(obj, 'description');
let description = objectToStringMap.get(obj);

组件复用场景

  1. UI组件库 在开发UI组件库时,很多组件需要支持多种数据类型。例如,一个下拉框组件可能需要显示不同类型的数据,如字符串、数字或者自定义对象。
interface Option<T> {
    label: string;
    value: T;
}

class Dropdown<T> {
    private options: Option<T>[] = [];
    addOption(label: string, value: T) {
        this.options.push({ label, value });
    }
    getOptions(): Option<T>[] {
        return this.options;
    }
}

这样,我们可以创建不同类型数据的下拉框,比如字符串类型的下拉框用于选择国家名称,数字类型的下拉框用于选择年龄等。

let countryDropdown = new Dropdown<string>();
countryDropdown.addOption('China', 'CN');
countryDropdown.addOption('USA', 'US');

let ageDropdown = new Dropdown<number>();
ageDropdown.addOption('18', 18);
ageDropdown.addOption('20', 20);
  1. 表格组件 表格组件通常需要展示不同类型的数据。我们可以设计一个泛型表格组件。
interface TableColumn<T> {
    key: keyof T;
    label: string;
}

class Table<T> {
    private data: T[] = [];
    private columns: TableColumn<T>[] = [];
    setData(data: T[]) {
        this.data = data;
    }
    addColumn(column: TableColumn<T>) {
        this.columns.push(column);
    }
    getColumns(): TableColumn<T>[] {
        return this.columns;
    }
    getData(): T[] {
        return this.data;
    }
}

通过这个泛型表格组件,我们可以根据不同的数据类型来定义表格的列和数据,比如展示用户信息(对象类型)的表格,展示订单金额(数字类型)的表格等。

interface User {
    name: string;
    age: number;
}

let userTable = new Table<User>();
userTable.addColumn({ key: 'name', label: 'Name' });
userTable.addColumn({ key: 'age', label: 'Age' });
let users: User[] = [
    { name: 'Alice', age: 25 },
    { name: 'Bob', age: 30 }
];
userTable.setData(users);

函数式编程场景

  1. 容器类与函子 在函数式编程中,函子(Functor)是一种带有 map 方法的容器类型。我们可以用泛型类来实现这样的容器。
class Container<T> {
    private value: T;
    constructor(value: T) {
        this.value = value;
    }
    map<U>(fn: (arg: T) => U): Container<U> {
        return new Container<U>(fn(this.value));
    }
}

这里 Container 类是一个简单的函子,它的 map 方法接受一个函数 fn,将 fn 应用到容器中的值,并返回一个新的容器。

let numberContainer = new Container<number>(5);
let stringContainer = numberContainer.map((num) => num.toString());
  1. Either类型 Either 类型表示两种可能的类型之一,通常用于处理可能失败的操作。
class Left<T> {
    value: T;
    constructor(value: T) {
        this.value = value;
    }
    map<U>(_fn: (arg: T) => U): Left<T> {
        return this;
    }
}

class Right<T> {
    value: T;
    constructor(value: T) {
        this.value = value;
    }
    map<U>(fn: (arg: T) => U): Right<U> {
        return new Right<U>(fn(this.value));
    }
}

type Either<L, R> = Left<L> | Right<R>;

例如,一个可能失败的除法操作可以用 Either 类型表示:

function safeDivide(a: number, b: number): Either<string, number> {
    if (b === 0) {
        return new Left<string>('Division by zero');
    } else {
        return new Right<number>(a / b);
    }
}

let result1 = safeDivide(10, 2);
let result2 = safeDivide(10, 0);

if (result1 instanceof Right) {
    console.log('Result:', result1.value);
} else {
    console.log('Error:', result1.value);
}

if (result2 instanceof Right) {
    console.log('Result:', result2.value);
} else {
    console.log('Error:', result2.value);
}

异步操作场景

  1. Promise包装类 虽然TypeScript已经有内置的 Promise 类型,但我们可以通过泛型类来进一步封装和定制异步操作。
class AsyncOperation<T> {
    private promise: Promise<T>;
    constructor(promise: Promise<T>) {
        this.promise = promise;
    }
    then<U>(onFulfilled: (value: T) => U | Promise<U>): AsyncOperation<U> {
        return new AsyncOperation<U>(this.promise.then(onFulfilled));
    }
    catch(onRejected: (reason: any) => void): AsyncOperation<T> {
        return new AsyncOperation<T>(this.promise.catch(onRejected));
    }
}

这样我们可以使用自定义的 AsyncOperation 类来处理异步操作,并且保持类型的一致性。

function asyncFunction(): Promise<number> {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(42);
        }, 1000);
    });
}

let operation = new AsyncOperation(asyncFunction());
operation.then((value) => {
    console.log('Processed value:', value * 2);
}).catch((error) => {
    console.log('Error:', error);
});
  1. 异步队列 有时候我们需要按顺序执行一系列异步操作,这可以通过异步队列来实现。
class AsyncQueue<T> {
    private tasks: (() => Promise<T>)[] = [];
    addTask(task: () => Promise<T>) {
        this.tasks.push(task);
    }
    async execute(): Promise<T[]> {
        let results: T[] = [];
        for (let task of this.tasks) {
            let result = await task();
            results.push(result);
        }
        return results;
    }
}

我们可以创建一个异步队列,并添加不同类型的异步任务。

function asyncTask1(): Promise<string> {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Task 1 completed');
        }, 1000);
    });
}

function asyncTask2(): Promise<number> {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(42);
        }, 2000);
    });
}

let queue = new AsyncQueue<unknown>();
queue.addTask(asyncTask1);
queue.addTask(asyncTask2);

queue.execute().then((results) => {
    console.log('Results:', results);
});

代码复用与可维护性提升

  1. 减少样板代码 在传统的面向对象编程中,如果我们需要为不同数据类型创建相似的类,会产生大量的样板代码。例如,为不同类型的排序算法创建类。但使用泛型类,我们可以创建一个通用的排序类。
class Sorter<T> {
    private data: T[];
    constructor(data: T[]) {
        this.data = data;
    }
    sort(compareFn: (a: T, b: T) => number): T[] {
        return this.data.sort(compareFn);
    }
}

这样,无论是对数字数组、字符串数组还是自定义对象数组进行排序,都可以使用这个 Sorter 泛型类。

let numberSorter = new Sorter<number>([3, 1, 2]);
let sortedNumbers = numberSorter.sort((a, b) => a - b);

let stringSorter = new Sorter<string>(['banana', 'apple', 'cherry']);
let sortedStrings = stringSorter.sort((a, b) => a.localeCompare(b));
  1. 提高代码的可维护性 当代码中存在大量重复代码时,维护和修改代码会变得困难。泛型类通过将通用逻辑抽象出来,使得代码更加简洁和易于维护。例如,如果我们需要修改排序算法的逻辑,只需要在 Sorter 泛型类中进行修改,所有使用该类的地方都会受到影响,而不需要在每个具体类型的排序类中分别修改。同时,泛型类的类型参数使得代码的类型安全性更高,减少了运行时类型错误的可能性。

与其他编程范式的融合

  1. 与面向对象编程的融合 泛型类本身就是面向对象编程的一部分,但它通过类型参数的灵活性,使得面向对象编程更加通用和强大。在一个大型的面向对象项目中,我们可以使用泛型类来创建基础的数据结构和工具类,这些类可以被不同的模块复用,并且保持类型的一致性。例如,一个游戏开发项目中,可能会使用泛型类来创建游戏对象池(ObjectPool<T>),不同类型的游戏对象(如角色、道具等)都可以使用这个对象池来管理,提高资源的复用效率。
  2. 与函数式编程的融合 如前面提到的函子和 Either 类型,泛型类可以很好地与函数式编程概念相结合。在函数式编程中,强调的是不可变数据和纯函数,泛型类可以作为一种容器来存储不可变的数据,并且通过 map 等方法来实现函数式的操作。同时,Either 类型这种泛型类的设计,有助于在处理可能失败的操作时,保持函数式编程的风格,避免副作用和错误处理的混乱。

通过以上对TypeScript泛型类设计与使用场景的详细阐述,我们可以看到泛型类在提高代码复用性、灵活性和可维护性方面具有重要作用,无论是在小型项目还是大型企业级应用开发中,都值得深入学习和应用。