2025-11-02 09:04:32

TypeScript 泛型终极指南:从懵圈到精通-(AI教我学编程)

TypeScript 泛型终极指南:从懵圈到精通

前言

如果你学 TypeScript 时被泛型搞得头晕目眩,恭喜你,你不是一个人。泛型(Generics)可能是 TypeScript 中最让初学者困惑的概念之一。那些满屏的 <T><K, V>extends 看起来就像天书。

但其实,泛型的核心思想出奇地简单。读完这篇文章,你会发现泛型不过是"类型层面的函数"而已。

一、为什么需要泛型?

问题:重复的代码

假设你要写一个函数,返回数组的第一个元素:

function getFirstNumber(arr: number[]): number { return arr[0]; } getFirstNumber([1, 2, 3]); // 可以用

但如果你有字符串数组呢?你得再写一个:

function getFirstString(arr: string[]): string { return arr[0]; }

对象数组?再写一个:

function getFirstObject(arr: object[]): object { return arr[0]; }

这也太蠢了!函数逻辑完全一样,唯一的区别只是类型不同

解决方案:泛型

function getFirst<T>(arr: T[]): T { return arr[0]; } // 一个函数,搞定所有类型 getFirst<number>([1, 2, 3]); // 返回 number getFirst<string>(['a', 'b', 'c']); // 返回 string getFirst([true, false]); // TypeScript 自动推断为 boolean

泛型让你写一次代码,适配所有类型。

二、泛型的本质:类型层面的函数

这是理解泛型的关键洞察:

// 普通函数:接收值,返回值 function double(x: number): number { return x * 2; } // 泛型:接收类型,返回类型 type ArrayOf<T> = T[];

让我们对比一下:

概念 普通函数 泛型
输入 值(如 5 类型(如 number
输出 值(如 10 类型(如 number[]
参数 (x: number) <T>
调用 double(5) ArrayOf<number>

泛型就是生成类型的函数,泛型参数就是这个函数的变量。

三、基础语法详解

1. 函数泛型

// 语法:function 函数名<泛型参数>(参数: 类型): 返回类型 function identity<T>(value: T): T { return value; } // 使用方式一:显式指定类型 identity<number>(42); // T = number identity<string>("hello"); // T = string // 使用方式二:类型推断(推荐) identity(42); // TypeScript 自动推断 T = number identity("hello"); // TypeScript 自动推断 T = string

2. 类型别名泛型

// 语法:type 类型名<泛型参数> = 类型定义 type Container<T> = { value: T; id: number; } // 使用 const numContainer: Container<number> = { value: 42, id: 1 }; const strContainer: Container<string> = { value: "hello", id: 2 };

3. 接口泛型

interface ApiResponse<T> { code: number; message: string; data: T; } // 使用 const userResponse: ApiResponse<User> = { code: 200, message: "成功", data: { id: 1, name: "张三" } }; const listResponse: ApiResponse<Article[]> = { code: 200, message: "成功", data: [ { title: "文章1", content: "内容1" }, { title: "文章2", content: "内容2" } ] };

四、实战案例:由浅入深

案例 1:数组操作

// 交换数组前两个元素 function swap<T>(arr: T[]): T[] { if (arr.length < 2) return arr; return [arr[1], arr[0], ...arr.slice(2)]; } swap([1, 2, 3]); // [2, 1, 3] swap(['a', 'b', 'c']); // ['b', 'a', 'c'] swap([true, false, true]); // [false, true, true]

案例 2:多个泛型参数

// 创建键值对 function makePair<K, V>(key: K, value: V): [K, V] { return [key, value]; } makePair("age", 25); // ["age", 25] makePair(1, "first"); // [1, "first"] makePair("user", { id: 100 }); // ["user", {id: 100}]

这里有两个类型参数:K(Key 的缩写)和 V(Value 的缩写)。

案例 3:Promise 类型(你肯定见过)

// Promise<T> 就是内置的泛型类型 async function fetchUser(): Promise<User> { const response = await fetch('/api/user'); return response.json(); // 返回 User 类型 } async function fetchNumber(): Promise<number> { return 42; }

Promise<User> 表示:这个 Promise 解析(resolve)后会得到 User 类型的数据。

五、泛型约束:限制类型的范围

有时候,你需要在泛型函数内部使用类型的某些特性(如属性或方法)。这时就需要泛型约束

问题场景

function getLength<T>(item: T): number { return item.length; // ❌ 报错!T 可能没有 length 属性 }

TypeScript 不知道 T 是否有 length 属性,所以报错。

解决方案:使用 extends 约束

// 约束 T 必须有 length 属性 function getLength<T extends { length: number }>(item: T): number { return item.length; // ✅ 安全! } getLength("hello"); // 5(字符串有 length) getLength([1, 2, 3]); // 3(数组有 length) getLength({ length: 10 }); // 10(对象有 length) // getLength(123); // ❌ 报错!数字没有 length

约束的本质

泛型约束就像函数参数的类型检查,确保传入的类型满足特定要求。

// 比喻:榨汁机只能榨"有汁水"的东西 interface Juiceable { squeeze(): string; } function makeJuice<T extends Juiceable>(fruit: T): string { return fruit.squeeze(); // 因为约束了 T 必须有 squeeze 方法 } class Orange implements Juiceable { squeeze() { return "🍊 橙汁"; } } class Apple implements Juiceable { squeeze() { return "🍎 苹果汁"; } } class Rock { // 没有 squeeze 方法 } makeJuice(new Orange()); // ✅ "🍊 橙汁" makeJuice(new Apple()); // ✅ "🍎 苹果汁" // makeJuice(new Rock()); // ❌ 报错!石头不能榨汁

六、常见的泛型模式

1. keyof 约束:限制为对象的键

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } const user = { name: "李四", age: 30, email: "li@example.com" }; const name = getProperty(user, "name"); // 类型是 string const age = getProperty(user, "age"); // 类型是 number // getProperty(user, "xxx"); // ❌ 报错!"xxx" 不是 user 的键

分解理解:

  • K extends keyof T:K 必须是 T 的键之一
  • keyof T:获取 T 的所有键组成的联合类型("name" | "age" | "email"
  • T[K]:获取对象 T 中键 K 对应值的类型

2. 默认类型参数

// 类似函数的默认参数 type Response<T = unknown> = { data: T; status: number; } type UserResponse = Response<User>; // T = User type DefaultResponse = Response; // T = unknown(使用默认值)

3. 泛型组合

type AddNull<T> = T | null; type AddArray<T> = T[]; // 组合使用 type NullableArray<T> = AddArray<AddNull<T>>; type Example = NullableArray<number>; // (number | null)[]

七、React 中的泛型(实战)

// 通用的列表组件 interface ListProps<T> { items: T[]; renderItem: (item: T) => React.ReactNode; } function List<T>({ items, renderItem }: ListProps<T>) { return ( <div> {items.map((item, index) => ( <div key={index}>{renderItem(item)}</div> ))} </div> ); } // 使用 interface User { id: number; name: string; } <List<User> items={users} renderItem={(user) => <span>{user.name}</span>} />

八、记忆诀窍

每次看到泛型时,心里默念这个公式:

type/function Name<参数 extends 约束 = 默认值> = 返回值/实现 // ↑ ↑ ↑ ↑ // 类型变量 限制条件 可选默认 类型定义

例如:

type MyArray<T extends object = {}> = T[]; // ↑ ↑ ↑ ↑ // 参数名 必须是对象 默认空对象 返回数组

九、常见命名约定

字母 含义 使用场景
T Type 通用类型参数
K Key 对象的键
V Value 对象的值
E Element 数组元素
P Props React 组件属性
S State React 组件状态

但记住:你可以用任何名字,比如 <数据类型><ItemType> 都可以。

十、总结

泛型的三个核心理解:

  1. 泛型 = 类型层面的函数

    • 接收类型参数,返回新类型
    • 就像函数接收值参数,返回新值
  2. 泛型参数 = 类型变量

    • <T> 就是一个占位符
    • 调用时才确定具体类型
  3. 泛型约束 = 原材料要求

    • 因为内部使用了类型的某些特性
    • 所以必须限制传入类型满足这些要求

下次看到复杂的泛型代码,试着这样拆解:

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

心里这样翻译:

  • 这是一个类型函数
  • 接收两个类型参数T(对象类型)和 K(键类型)
  • K约束:必须是 T 的键
  • 返回类型T[K](对象中该键对应值的类型)

现在,去征服那些曾经让你头晕的泛型代码吧!


练习建议:把文中的例子都自己敲一遍,改改参数试试会发生什么。泛型不是用来背的,是用来理解的。当你能流畅地读懂 Promise<T>Array<T>Record<K, V> 这些内置泛型时,你就真正掌握了。

本文链接:http://blog.go2live.cn/post/typescript-generic.html

-- EOF --