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 中排除 null 和 undefined。

    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 强大类型系统的基石之一,掌握它能够让你编写出更健壮、更灵活、更易于维护的代码。

相关推荐
卸任3 小时前
Vibe coding工具使用心得
前端·ai编程
brien94 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance4 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M4 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写4 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李4 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易5 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马5 小时前
前沿视界
前端
kyriewen5 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易5 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员