TypeScript 类、泛型的使用实践记录 | 青训营

91 阅读7分钟

在TypeScript中,泛型是一种强大的特性,它允许我们在定义函数、类或接口时使用参数化类型,从而实现更灵活、可复用的代码。泛型使得我们可以编写不依赖于具体类型的代码,使代码更加通用且类型安全。本文将围绕三个方面来介绍泛型的相关知识:

首先,让我们先回顾一下上节课对泛型的基本定义与使用:

image.png

泛型的使用方法

  1. 泛型函数
function identity<T>(arg: T): T {
    return arg;
}

在这个例子中,<T>定义了一个泛型参数 T,表示函数参数和返回值都是类型 T

  1. 泛型约束
interface Lengthy {
    length: number;
}

function printLength<T extends Lengthy>(arg: T): void {
    console.log(arg.length);
}

在这个例子中,使用 extends 关键字来约束泛型参数 T 必须是带有 length 属性的类型。

  1. 泛型类
class Box<T> {
    private value: T;

    constructor(value: T) {
        this.value = value;
    }

    getValue(): T {
        return this.value;
    }
}

在这个例子中,我们定义了一个泛型类 Box<T>,它有一个私有属性 value 和两个方法:构造函数和 getValue 方法。属性 value 的类型为 T,并在构造函数中接受一个类型为 T 的参数来初始化。

我们可以使用泛型类来创建不同类型的 Box 对象,并在实例化时指定 T 的具体类型。例如:

const box1 = new Box<number>(42); // 创建一个 Box<number> 类型的实例,存储 number 类型的值
const box2 = new Box<string>('Hello'); // 创建一个 Box<string> 类型的实例,存储 string 类型的值

console.log(box1.getValue()); // 输出: 42
console.log(box2.getValue()); // 输出: Hello
  1. 泛型接口
interface Pair<K, V> {
    key: K;
    value: V;
}

function printPair<K, V>(pair: Pair<K, V>): void {
    console.log(pair.key, pair.value);
}

在这个例子中,我们定义了一个泛型接口 Pair<K, V>,它有两个属性 keyvalue,分别表示键和值,并且这两个属性的类型分别由泛型参数 KV 决定。

我们还定义了一个泛型函数 printPair<K, V>(pair: Pair<K, V>): void,它接受一个类型为 Pair<K, V> 的参数,并输出该参数的 keyvalue 属性。

现在,我们可以使用泛型接口 Pair 和泛型函数 printPair 来处理不同类型的键值对:

const pair1: Pair<string, number> = { key: 'age', value: 30 };
const pair2: Pair<string, boolean> = { key: 'isStudent', value: true };

printPair(pair1); // 输出: age 30
printPair(pair2); // 输出: isStudent true
  1. 泛型约束在键值对中的应用
interface KeyValue<K, V> {
    key: K;
    value: V;
}

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

在这个例子中,我们定义了一个泛型接口 KeyValue<K, V>,它有两个属性 keyvalue,分别表示键和值,并且这两个属性的类型分别由泛型参数 KV 决定。

然后,我们定义了一个泛型函数 getValueByKey<T extends object, K extends keyof T>(obj: T, key: K): T[K],这个函数接受一个类型为 T 的对象 obj 和一个类型为 K 的键名 key,并返回对应键名的值。

在这里,我们使用了泛型约束来限制传入的参数。T extends object 表示传入的 obj 必须是一个对象类型。K extends keyof T 表示传入的 key 必须是 T 对象的一个合法键名。

这样做的好处是,在使用 getValueByKey 函数时,编译器会对参数类型进行检查。如果传入的 obj 不是对象类型,或者传入的 key 不是 obj 对象的一个键名,编译器将会报错,从而在编译阶段就避免了一些潜在的错误。

示例如下:

const person = {
    name: 'John',
    age: 30,
    isStudent: true,
};

const name = getValueByKey(person, 'name'); // 类型为 string
const age = getValueByKey(person, 'age'); // 类型为 number
const isStudent = getValueByKey(person, 'isStudent'); // 类型为 boolean
const invalidKey = getValueByKey(person, 'invalidKey'); // 报错,无效的键名

泛型的使用场景

  1. 集合类型:泛型常用于数组、集合、栈、队列等数据结构的定义,以及它们的操作函数。通过泛型,我们可以在不同场景下使用不同类型的集合,使代码更加通用。
  2. 类型安全的工具函数:通过使用泛型,我们可以编写一些类型安全的工具函数,如mapfilterreduce等。这些函数可以适用于不同类型的数组,而不必重复编写多个类似的函数。
  3. React 组件:在 React 组件中,Props 和 State 都可以使用泛型,以实现更灵活的组件。例如,可以创建一个通用的列表组件,接受不同类型的数据作为列表项,并渲染不同类型的列表。
  4. 数据结构的封装:泛型允许我们编写通用的数据结构,如树、图、链表等。这些数据结构可以适用于不同类型的数据。
  5. 异步操作:在处理异步操作时,泛型可以帮助我们定义函数返回值的类型,并提供类型安全的链式调用。例如,在 Promise 中使用泛型来指定异步操作的结果类型。
  6. 数据库操作:在使用数据库时,泛型可以帮助我们定义通用的增删改查函数,从而适用于不同类型的数据表。
  7. 插件和库开发:在编写通用的插件或库时,使用泛型可以使插件适用于不同类型的应用程序或环境。

如何使用类型约束来增加代码的灵活性和安全性

使用类型约束是 TypeScript 中提高代码灵活性和安全性的重要手段。类型约束可以帮助我们在编写代码时对类型进行限制,从而确保代码只接受特定类型的参数,并提供更严格的类型检查和错误捕获。下面是一些使用类型约束的方法来增加代码灵活性和安全性:

  1. 函数参数类型约束: 在函数中使用参数的类型约束可以确保传入的参数满足特定的类型要求。这可以避免不正确的参数类型导致的错误。例如:

    typescriptCopy code
    function calculateTotal(price: number, quantity: number): number {
        return price * quantity;
    }
    

    在上面的例子中,pricequantity 都被约束为 number 类型,确保了函数只能接受数字类型的参数。

  2. 返回值类型约束: 通过为函数指定返回值类型约束,可以确保函数返回的值符合预期的类型。这有助于避免在函数中返回错误的类型。例如:

    typescriptCopy code
    function getFullName(firstName: string, lastName: string): string {
        return firstName + ' ' + lastName;
    }
    

    在上面的例子中,我们明确指定了函数的返回值类型为 string

  3. 泛型约束: 使用泛型约束可以限制泛型类型参数的范围,从而确保泛型在特定的类型条件下工作。例如,我们可以使用泛型约束来确保传入的对象具有特定的属性或方法。这在封装通用代码时非常有用。

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

    在上面的例子中,泛型约束 K extends keyof T 确保传入的 key 必须是 obj 对象的一个合法键名。

  4. 接口约束: 使用接口来约束对象的结构,确保对象具有特定的属性和方法。这有助于在编译时捕获对象结构错误。

    typescriptCopy code
    interface Person {
        name: string;
        age: number;
    }
    
    function greet(person: Person): string {
        return `Hello, ${person.name}!`;
    }
    

    在上面的例子中,person 参数必须满足 Person 接口的结构。

  5. 类约束: 使用类来约束对象的行为,确保对象具有特定的方法和属性。这有助于确保代码在调用对象方法时不会出现未定义的错误。

    typescriptCopy code
    class Calculator {
        add(a: number, b: number): number {
            return a + b;
        }
    }
    
    const calc = new Calculator();
    const result = calc.add(2, 3);
    

    在上面的例子中,calc 实例必须具有 add 方法。

  6. 联合类型和交叉类型: 使用联合类型和交叉类型可以将多个类型组合在一起,从而增加代码的灵活性。例如,使用联合类型来表示一个值可以是多个类型中的一种。

    typescriptCopy code
    function printValue(value: string | number) {
        console.log(value);
    }
    

    在上面的例子中,value 参数可以是 stringnumber 类型。

    交叉类型则如下:

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

interface Employee {
    company: string;
    position: string;
}

type EmployeePerson = Person & Employee;

const employeePerson: EmployeePerson = {
    name: 'John',
    age: 30,
    company: 'ABC Inc.',
    position: 'Manager',
};