TS泛型笔记

泛型(Generics)是 TypeScript 中一个非常强大和灵活的特性,它允许你编写可重用的、类型安全的代码。泛型可以应用于函数、接口、类和类型别名,使得这些组件能够处理多种数据类型,而不是局限于单一类型。

1. 为什么需要泛型?(解决什么问题?)

在没有泛型的情况下,我们可能会遇到以下问题:

  1. 丢失类型信息 : 当我们希望一个函数能处理多种类型的数据,但又想保持类型安全时,可能会使用 any 类型。然而,any 会导致我们失去类型检查的优势,无法在编译时捕获错误。

    js 复制代码
    function identity(arg: any): any {
        return arg;
    }
    let output = identity("myString"); // output 的类型是 any
    console.log(output.length); // 运行时可能出错,因为 any 失去了类型检查
  2. 代码重复或过度重载: 为了处理不同类型,我们可能需要为每种类型编写单独的函数重载。这会导致代码重复且难以维护。

    js 复制代码
    function identityString(arg: string): string {
        return arg;
    }
    function identityNumber(arg: number): number {
        return arg;
    }
    // 如果有更多类型,就需要更多重载

泛型解决了这些问题 :它引入了类型变量的概念,允许我们在定义函数、接口或类时,不预先指定具体类型,而是在使用时才确定类型。这就像函数参数一样,你定义函数时使用参数名,调用时才传入具体的值。

2. 泛型的基本语法和使用

泛型使用尖括号 <> 来声明类型变量,通常用单个大写字母表示,如 T(Type)、K(Key)、V(Value)等。

2.1 泛型函数

这是最常见的泛型使用场景。

js 复制代码
// 泛型函数 identity
// <T> 声明了一个类型变量 T
// arg: T 表示参数 arg 的类型是 T
// : T 表示函数的返回值类型也是 T
function identity<T>(arg: T): T {
    return arg;
}

// 使用泛型函数
// 方式一:明确指定类型参数 (不常用,因为 TypeScript 通常能推断出来)
let output1 = identity<string>("myString"); // output1 的类型是 string
console.log(output1.length); // 编译时检查通过

let output2 = identity<number>(123); // output2 的类型是 number
// console.log(output2.length); // 编译时报错:Property 'length' does not exist on type 'number'.

// 方式二:利用类型推断 (更常用)
// TypeScript 编译器会根据传入的参数自动推断出 T 的类型
let output3 = identity("anotherString"); // T 被推断为 string
let output4 = identity(true);            // T 被推断为 boolean

2.2 泛型接口

泛型接口允许我们定义一个接口,其成员的类型取决于接口使用时指定的类型。

js 复制代码
// 定义一个泛型接口
interface GenericIdentityFn<T> {
    (arg: T): T;
}

// 实现这个泛型接口
// 这里的 identity 函数与上面的泛型函数相同
function identity<T>(arg: T): T {
    return arg;
}

// 使用泛型接口时,需要指定类型参数
let myIdentity: GenericIdentityFn<number> = identity;
console.log(myIdentity(42)); // 42 (number 类型)

// let myIdentity2: GenericIdentityFn<string> = identity;
// console.log(myIdentity2("hello")); // "hello" (string 类型)

2.3 泛型类

泛型类与泛型接口类似,允许类在实例化时指定类型。

js 复制代码
class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;

    constructor(zero: T, adder: (x: T, y: T) => T) {
        this.zeroValue = zero;
        this.add = adder;
    }
}

// 实例化一个处理数字的 GenericNumber 类
let myGenericNumber = new GenericNumber<number>(0, (x, y) => x + y);
console.log(myGenericNumber.add(5, 10)); // 15

// 实例化一个处理字符串的 GenericNumber 类
let myGenericString = new GenericNumber<string>("", (x, y) => x + y);
console.log(myGenericString.add("Hello, ", "TypeScript!")); // "Hello, TypeScript!"

3. 使用泛型类型

3.1 泛型约束 (Generic Constraints)

有时我们希望泛型类型具有某些特定的属性或方法。例如,我们可能想访问 arg.length,但 T 可以是任何类型,不一定有 length 属性。这时就需要使用泛型约束。

泛型约束通过 extends 关键字实现。

ts 复制代码
interface Lengthwise {
    length: number;
}

// T 必须是实现了 Lengthwise 接口的类型
function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length); // 现在我们可以确定 arg 具有 length 属性
    return arg;
}

// 使用时,传入的类型必须满足约束
loggingIdentity({ length: 10, value: 3 }); // OK
loggingIdentity("hello");                   // OK, string 具有 length 属性
loggingIdentity([1, 2, 3]);                 // OK, array 具有 length 属性

// loggingIdentity(3); // 编译时报错:Argument of type '3' is not assignable to parameter of type 'Lengthwise'.

3.2 在泛型约束中使用类型参数

你可以声明一个类型参数,它受另一个类型参数的约束。这在处理对象属性时非常有用。

ts 复制代码
// obj 的类型是 T,key 的类型是 K
// K 必须是 T 的属性名 (keyof T)
function getProperty<T, K extends keyof T>(obj: T, key: K) {
    return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a"); // OK, returns 1
// getProperty(x, "m"); // 编译时报错:Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.

3.3 泛型与类类型

在创建工厂函数时,泛型可以用来确保返回的实例类型正确。

ts 复制代码
// 泛型函数,接受一个构造函数类型 C,并返回该类型的一个实例
function create<T>(c: { new (): T; }): T {
    return new c();
}

class BeeKeeper {
    hasMask: boolean = true;
}

class Zookeeper {
    nametag: string = "Miley";
}

class Animal {
    numLegs: number = 4;
}

class Lion extends Animal {
    mane: boolean = true;
}

class Gopher extends Animal {
    burrow: boolean = true;
}

// 使用 create 函数创建实例
let lion = create(Lion); // lion 的类型是 Lion
let gopher = create(Gopher); // gopher 的类型是 Gopher

// 结合泛型约束
// T 必须是 Animal 的子类,并且构造函数类型也必须是 Animal 的子类
function createAnimal<T extends Animal>(c: { new (): T; }): T {
    return new c();
}

let lion2 = createAnimal(Lion); // lion2 的类型是 Lion
// let beeKeeper = createAnimal(BeeKeeper); // 编译时报错:Argument of type 'typeof BeeKeeper' is not assignable to parameter of type 'new () => Animal'.

4. 泛型 vs. any vs. 函数重载

  • any: 放弃了所有类型检查,失去了 TypeScript 的核心优势。
  • 函数重载: 适用于处理少数几种确定类型的情况。当类型数量增多或类型不确定时,重载会变得非常冗长和难以维护。
  • 泛型: 是处理不确定类型数量和保持类型安全的最佳选择。它提供了灵活性和类型检查的平衡。

5. 高级泛型话题

5.1 泛型默认类型 (Default Generic Types)

你可以为泛型参数提供一个默认类型,当不显式指定类型参数时,就会使用这个默认类型。

ts 复制代码
interface Box<T = string> { // 如果不指定 T,T 默认为 string
    value: T;
}

let stringBox: Box = { value: "hello" }; // T 默认为 string
let numberBox: Box<number> = { value: 123 }; // T 显式指定为 number

// stringBox.value = 456; // 编译时报错:Type '456' is not assignable to type 'string'.

5.2 条件类型 (Conditional Types) 与泛型

条件类型允许你根据一个类型是否符合某些条件来选择不同的类型。它们通常与泛型结合使用。

ts 复制代码
// 如果 T 扩展自 U,则类型为 X,否则为 Y
type TypeName<T> =
    T extends string ? "string" :
    T extends number ? "number" :
    T extends boolean ? "boolean" :
    T extends undefined ? "undefined" :
    T extends Function ? "function" :
    "object";

type T1 = TypeName<string>;   // "string"
type T2 = TypeName<number>;   // "number"
type T3 = TypeName<boolean>;  // "boolean"
type T4 = TypeName<() => void>; // "function"
type T5 = TypeName<{}>;      // "object"
type T6 = TypeName<null>;    // "object" (因为 null 也是 object)

5.3 映射类型 (Mapped Types) 与泛型

映射类型允许你基于现有类型创建新类型,通过遍历旧类型的所有属性来创建新属性。它们通常与泛型结合使用。

ts 复制代码
// 将 T 的所有属性变为只读
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

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

type ReadonlyPerson = Readonly<Person>;
// ReadonlyPerson 的类型是:
// {
//   readonly name: string;
//   readonly age: number;
// }

// ReadonlyPerson.name = "Jane"; // 编译时报错:Cannot assign to 'name' because it is a read-only property.

5.4 内置泛型工具类型 (Utility Types)

TypeScript 提供了一系列内置的泛型工具类型,它们可以帮助你进行常见的类型转换操作。

  • Partial<T> : 将 T 的所有属性变为可选。

    ts 复制代码
    interface Todo { title: string; description: string; }
    type PartialTodo = Partial<Todo>; // { title?: string; description?: string; }
  • Readonly<T> : 将 T 的所有属性变为只读。

    ts 复制代码
    type ReadonlyTodo = Readonly<Todo>; // { readonly title: string; readonly description: string; }
  • Pick<T, K> : 从 T 中选择一组属性 K 来构成新类型。

    ts 复制代码
    type TodoTitle = Pick<Todo, "title">; // { title: string; }
  • Omit<T, K> : 从 T 中排除一组属性 K 来构成新类型。

    ts 复制代码
    type TodoWithoutDescription = Omit<Todo, "description">; // { title: string; }
  • Exclude<T, U> : 从 T 中排除可分配给 U 的类型。

    ts 复制代码
    type T7 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
  • Extract<T, U> : 从 T 中提取可分配给 U 的类型。

    ts 复制代码
    type T8 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
  • NonNullable<T> : 从 T 中排除 nullundefined

    ts 复制代码
    type T9 = NonNullable<string | number | undefined | null>; // string | number
  • Parameters<T> : 获取函数类型 T 的参数类型组成的元组。

    ts 复制代码
    function greet(name: string, age: number) { /* ... */ }
    type Params = Parameters<typeof greet>; // [name: string, age: number]
  • ReturnType<T> : 获取函数类型 T 的返回值类型。

    ts 复制代码
    type Return = ReturnType<typeof greet>; // void
  • InstanceType<T> : 获取构造函数类型 T 的实例类型。

    ts 复制代码
    class MyClass { x: number; constructor(x: number) { this.x = x; } }
    type Instance = InstanceType<typeof MyClass>; // MyClass
  • Awaited<T> : 获取 Promise 的解决类型。

    ts 复制代码
    type PromiseResult = Awaited<Promise<string>>; // string

6. 泛型最佳实践

  • 使用有意义的类型变量名:

    • 单个字母通常用于简单泛型 (T, K, V)。
    • 更具描述性的名称用于复杂场景 (TElement, TProps, TState, TArgs)。
  • 尽可能使用泛型约束:

    • 这不仅能提供更好的类型安全,还能让 TypeScript 编译器更好地理解你的意图,从而提供更准确的类型推断和错误检查。
  • 避免过度泛型化:

    • 如果一个组件只处理少数几种确定类型,函数重载可能更清晰。
    • 不要为了使用泛型而使用泛型,它应该解决实际问题。
  • 文档化泛型参数:

    • 在复杂组件中,使用 JSDoc 或其他注释工具解释每个泛型参数的用途和约束。

泛型是 TypeScript 强大类型系统的基石之一,掌握它能够让你编写出更健壮、更灵活、更易于维护的代码。

相关推荐
10年前端老司机30 分钟前
10道js经典面试题助你找到好工作
前端·javascript
小小小小宇6 小时前
前端canvas手动实现复杂动画示例
前端
codingandsleeping6 小时前
重读《你不知道的JavaScript》(上)- 作用域和闭包
前端·javascript
小小小小宇6 小时前
前端PerformanceObserver使用
前端
zhangxingchao7 小时前
Flutter中的页面跳转
前端
烛阴8 小时前
Puppeteer入门指南:掌控浏览器,开启自动化新时代
前端·javascript
全宝9 小时前
🖲️一行代码实现鼠标换肤
前端·css·html
小小小小宇9 小时前
前端模拟一个setTimeout
前端
萌萌哒草头将军9 小时前
🚀🚀🚀 不要只知道 Vite 了,可以看看 Farm ,Rust 编写的快速且一致的打包工具
前端·vue.js·react.js