16.泛型 副本.md 来源:https://my.feishu.cn/file/AVOGbeETUoSAbvxuRHucRulinob 采集状态:已完成正文采集;已从查看代码模式逐行提取,行号 1 至 161 连续,并核对滚动到底。 在我们的编程旅程中,一个永恒的追求是编写**可复用**的代码。我们希望创建一个函数或一个类,它能够处理多种不同的数据类型,而不仅仅是一种。然而,在追求复用性的同时,我们如何才能不牺牲 TypeScript 提供的最大优势——类型安全呢? 这正是**泛型 (Generics)** 登场解决的核心问题。泛型是 TypeScript 中最强大的特性之一,它允许我们创建可以“适用于多种类型”的组件,同时还能保持完整的类型信息和安全检查。可以说,掌握泛型,是从“会用” TypeScript 到“精通” TypeScript 的关键一步。 ### 问题的起点:`any` 类型的陷阱 让我们从一个简单的需求开始:编写一个 `identity` 函数,它接收一个参数并原封不动地返回它。 一种最直接、但也是最危险的实现方式是使用 `any` 类型: ```typescript function identity(arg: any): any { return arg; } let output = identity("myString"); // output 的类型是 any ``` 这段代码虽然能工作,但我们却付出了巨大的代价:**丢失了类型信息**。我们传入了一个 `string`,但 `output` 变量的类型却变成了 `any`。这意味着,我们可以在 `output` 上进行任何操作(比如 `output.toFixed(2)`),TypeScript 编译器都不会发出任何警告,但这些操作在运行时很可能会引发错误。我们等于亲手关闭了 TypeScript 的类型保护。 我们需要一种方法,既能让函数接受任意类型,又能确保返回值的类型与传入参数的类型完全一致。 ### 泛型的解决方案:类型变量 泛型通过引入**类型变量 (Type Variables)** 来解决这个问题。类型变量是一种特殊的变量,它不代表值,而是代表**类型**。它就像一个占位符,当我们使用该组件时,再填入具体的类型。 让我们用泛型来重写 `identity` 函数: ```typescript function identity(arg: T): T { return arg; } ``` 这段代码的变化看似微小,但意义深远。让我们来分解这个语法: 1. **``**:这是类型变量的声明。我们告诉 TypeScript,`T` 是一个类型占位符。`T` 这个名字是惯例,你也可以用任何其他合法的变量名(如 `Type`, `TInput` 等)。 2. **`arg: T`**:我们将参数 `arg` 的类型设置为 `T`。 3. **`: T`**:我们指定函数的返回值类型也是 `T`。 通过这种方式,我们建立了一个契约:**传入参数的类型与返回值的类型必须一致**。`T` 在函数被调用时,才被具体的类型所填充。 现在,让我们看看调用这个泛型函数时会发生什么: ```typescript // 1. 显式指定类型 let output = identity("myString"); // output 的类型是 string // 2. 利用类型推断 (更常见) let anotherOutput = identity(123); // output 的类型是 number ``` 在第二个例子中,我们没有显式地用 `` 来指定类型,但 TypeScript 非常智能,它会根据我们传入的参数 `123`,自动**推断**出 `T` 应该被 `number` 替代。 现在,我们得到了两全其美的结果:一个可以处理任何类型的函数,并且完全保留了类型信息,类型安全得到了百分之百的保障。 ### 泛型的广阔天地 泛型的应用远不止于函数。它可以被用于接口、类,从而构建出高度可复用且类型安全的系统。 #### 1. 泛型接口 我们可以定义一个泛型接口来描述一个通用的数据结构,比如 API 的响应格式。 ```typescript interface ApiResponse { data: T; status: 'success' | 'error'; message?: string; } // 定义 User 类型 interface User { id: number; name: string; } // 使用泛型接口 const userResponse: ApiResponse = { data: { id: 1, name: "Raycast" }, status: "success", }; const errorResponse: ApiResponse = { data: null, status: "error", message: "Not found", }; ``` `ApiResponse` 成为了一个可复用的模板,可以用来包裹任何类型的 `data`。 #### 2. 泛型类 同样,泛型也可以用于类,以创建能处理多种数据类型的容器或集合。 ```typescript class DataStore { private data: T[] = []; addItem(item: T): void { this.data.push(item); } getItems(): T[] { return [...this.data]; } } // 创建一个只能存储数字的实例 const numberStore = new DataStore(); numberStore.addItem(10); // numberStore.addItem("hello"); // 编译时错误! // 创建一个只能存储字符串的实例 const stringStore = new DataStore(); stringStore.addItem("hello"); ``` ### 泛型约束:为灵活性加上“缰绳” 有时候,我们希望泛型类型 `T` 不是完全任意的,而是必须满足某些特定的条件。例如,我们想编写一个函数,它接收一个参数并打印其 `length` 属性。 一个错误的尝试是: ```typescript // function logLength(arg: T): void { // console.log(arg.length); // 编译时错误: Property 'length' does not exist on type 'T'. // } ``` 编译器会报错,因为它无法保证任意类型 `T` 都拥有 `length` 属性。 为了解决这个问题,我们需要使用**泛型约束 (Generic Constraints)**。我们通过 `extends` 关键字,来要求类型变量 `T` 必须“继承”自某个特定的类型。 ```typescript interface Lengthwise { length: number; } // T 必须是拥有 length: number 属性的类型 function logLength(arg: T): void { console.log(arg.length); } logLength("hello"); // string 有 length 属性 logLength([1, 2, 3]); // array 有 length 属性 logLength({ length: 10, value: '...' }); // 对象有 length 属性 // logLength(123); // 编译时错误: number 没有 length 属性 ``` 通过 `extends Lengthwise`,我们与编译器达成了一个新的约定:`T` 可以是任何类型,但它**必须**符合 `Lengthwise` 接口的契约。这既保证了函数的灵活性,又确保了在函数内部可以安全地调用 `.length`。 ### 总结 泛型是 TypeScript 类型系统的精髓所在,它是在抽象能力和类型安全之间架起的一座完美桥梁。 * 它通过**类型变量** (``) 作为类型的占位符,让我们能够编写出与具体类型无关的可复用组件。 * 它在编译时进行类型检查,**保留了完整的类型信息**,避免了 `any` 类型带来的风险。 * 通过**泛型约束** (`extends`),我们可以对类型变量的范围进行限制,确保其具备某些必要的属性或方法。 当你发现自己在为不同的数据类型编写功能相似的函数或类时,停下来想一想,这正是泛型大显身手的绝佳时机。拥抱泛型,你将能够构建出更优雅、更健壮、更具扩展性的 TypeScript 应用程序。