5-9 泛型
泛型基础概念
泛型定义
在定义函数、接口或类时,不预先指定具体类型,而是在使用时再动态指定类型的特性。泛型允许开发者编写灵活且类型安全的代码,避免重复逻辑。
示例:
// 非泛型函数:只能处理特定类型
function identityString(arg: string): string {
return arg;
}
// 泛型函数:支持多种类型
function identity<T>(arg: T): T {
return arg;
}
// 使用
identity<string>("Hello"); // 显式指定类型
identity(42); // 自动推断为number
typescript
💡 泛型参数通常用大写字母表示(如 T
, U
, V
),但也可以是其他名称,如 Key
, Value
。
泛型核心价值
- 提高代码复用性
- 避免为不同类型编写重复逻辑。
- 示例:一个排序函数可以同时支持
number[]
和string[]
。
- 增强灵活性
- 将类型决定权交给调用者,而非函数定义者。
- 示例:一个数据存储类可以动态指定存储的数据类型。
- 保持类型安全
- 编译时进行类型检查,避免运行时错误。
- 示例:泛型约束确保传入的参数满足特定条件。
实践案例:
// 泛型接口
interface Response<T> {
data: T;
status: number;
}
// 使用
const userResponse: Response<{ name: string }> = {
data: { name: "Alice" },
status: 200,
};
const productResponse: Response<{ id: number }> = {
data: { id: 1 },
status: 200,
};
typescript
泛型与动态类型的区别
特性 | 泛型 | 动态类型(如 any ) |
---|---|---|
类型检查 | 编译时类型安全 | 无类型检查 |
灵活性 | 支持类型约束和推导 | 完全自由,但易出错 |
适用场景 | 需要类型安全的复用逻辑 | 快速原型开发或兼容旧代码 |
💡 避免滥用 any
,优先使用泛型以保持类型安全!
常见问题解答
Q:泛型会增加运行时性能开销吗?
A:不会。泛型是编译时特性,TypeScript 会在编译后擦除类型信息,生成的 JavaScript 代码与非泛型版本相同。
Q:泛型可以嵌套吗?
A:可以。例如:
function nestedGeneric<T, U>(arg1: T, arg2: U): [T, U] {
return [arg1, arg2];
}
typescript
Q:如何限制泛型的类型范围?
A:使用 extends
关键字:
function logLength<T extends { length: number }>(arg: T): void {
console.log(arg.length);
}
logLength("text"); // ✅
logLength([1, 2, 3]); // ✅
logLength(42); // ❌ 没有length属性
typescript
延伸学习资源
- TypeScript 官方泛型文档
- 推荐书籍:《Effective TypeScript》第3章
- 实战练习:尝试用泛型重构一个已有的工具函数库 🚀
通过泛型,你可以写出更简洁、更安全的代码,同时保持高度的灵活性!
泛型函数示例
问题场景分析
在开发中,我们经常需要为不同类型的数据编写几乎相同的函数逻辑。例如,以下两个函数分别用于向字符串数组和数字数组添加元素:
// 处理字符串数组的函数
function pushStringArr(arr: string[], item: string): string[] {
arr.push(item);
return arr;
}
// 处理数字数组的函数
function pushNumberArr(arr: number[], item: number): number[] {
arr.push(item);
return arr;
}
typescript
问题:
- 代码重复率高,维护成本增加。
- 新增类型(如
boolean[]
)需要编写新函数,扩展性差。 - 违反 DRY(Don’t Repeat Yourself)原则。
💡 这种场景是泛型的典型应用场景!
泛型解决方案
通过泛型,我们可以将类型参数化,避免重复代码:
// 泛型函数:支持任意类型的数组和元素
function pushArr<T>(arr: T[], item: T): T[] {
arr.push(item);
return arr;
}
typescript
关键点解析:
<T>
:声明泛型参数T
,表示动态类型。arr: T[]
:输入是一个T
类型的数组。item: T
:要添加的元素必须与数组类型一致。- 返回值
T[]
:返回更新后的数组。
使用示例
// 数字数组
const numArr = [1, 2, 3];
pushArr<number>(numArr, 4); // ✅ 正确
console.log(numArr); // [1, 2, 3, 4]
// 字符串数组
const strArr = ["a", "b"];
pushArr<string>(strArr, "c"); // ✅ 正确
console.log(strArr); // ["a", "b", "c"]
// 类型错误示例
pushArr<string>(strArr, 123); // ❌ 错误:number 不能赋值给 string
typescript
💡 类型推断:如果调用时省略 <T>
,TypeScript 会自动推断类型:
pushArr(numArr, 4); // 自动推断为 number
typescript
进阶用法
1. 默认泛型参数
可以为泛型参数指定默认类型:
function createArray<T = string>(length: number, value: T): T[] {
return Array(length).fill(value);
}
const strArray = createArray(3, "hello"); // string[]
const numArray = createArray<number>(3, 42); // number[]
typescript
2. 多泛型参数
支持多个泛型参数,适用于更复杂的场景:
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const result = pair<string, number>("age", 30); // [string, number]
typescript
3. 泛型约束
通过 extends
限制泛型范围:
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(arg: T): void {
console.log(arg.length);
}
logLength("text"); // ✅ 输出:4
logLength([1, 2, 3]); // ✅ 输出:3
logLength(42); // ❌ 错误:number 没有 length 属性
typescript
常见问题解答
Q:泛型函数和函数重载有什么区别?
- 泛型函数:通过类型参数化实现逻辑复用,适用于类型不同但逻辑相同的场景。
- 函数重载:为不同类型提供不同的实现,适用于逻辑不同的场景。
Q:泛型会影响运行时性能吗?
A:不会。泛型是编译时特性,TypeScript 会在编译后擦除类型信息。
Q:如何调试泛型函数?
A:在开发工具中,鼠标悬停泛型函数调用处,可以查看类型推断结果。
实战练习
- 重构代码:将项目中重复的类型特定函数改为泛型函数。
- 扩展功能:实现一个泛型
filter
函数,支持过滤任意类型的数组。 - 挑战:尝试用泛型实现一个通用的
Cache
类,支持存储任意类型的数据。
通过泛型,你可以大幅减少重复代码,同时保持类型安全和灵活性!
多元组交换示例
泛型函数实现详解
这个泛型函数swapGeneric
展示了如何利用泛型实现类型安全的元组元素交换:
function swapGeneric<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}
typescript
关键点解析:
- 双泛型参数:使用
T
和U
分别表示元组中两个元素的类型 - 输入类型:参数是
[T, U]
类型的元组 - 返回值:返回
[U, T]
类型的新元组 - 实现逻辑:简单交换元组中两个元素的位置
💡 这个实现完美体现了泛型的价值:
- 类型安全:编译器会确保输入和输出的类型正确对应
- 代码复用:一套逻辑适用于所有类型的元组
- 灵活性:不限制具体类型,由调用者决定
使用场景深入分析
基础使用
const result = swapGeneric<string, number>(["text", 123]);
console.log(result); // [123, "text"]
typescript
类型推导过程:
- 调用时显式指定
T=string
,U=number
- 输入元组类型为
[string, number]
- 返回值类型自动推导为
[number, string]
类型提示保留
// 交换后第一个元素是number类型
result[0].toFixed(2); // ✅ 可以调用number的方法
// 第二个元素是string类型
result[1].replace("t", "T"); // ✅ 可以调用string的方法
typescript
自动类型推断
const inferred = swapGeneric(["hello", true]);
// 自动推断为 T=string, U=boolean
// 返回类型为 [boolean, string]
typescript
进阶应用场景
1. 对象属性交换
function swapProps<T, U>(obj: {a: T; b: U}): {a: U; b: T} {
return {a: obj.b, b: obj.a};
}
const obj = {a: "name", b: 100};
const swapped = swapProps(obj);
// swapped类型为 {a: number, b: string}
typescript
2. 多类型组合
const complexSwap = swapGeneric<[string, boolean], number>(
[["text", true], 42]
);
// 返回类型为 [number, [string, boolean]]
typescript
3. 配合类型约束
function swapWithConstraint<T extends object, U extends number>(
tuple: [T, U]
): [U, T] {
return [tuple[1], tuple[0]];
}
typescript
错误处理示例
// 类型不匹配错误
swapGeneric<string, number>([123, "text"]);
// ❌ 错误:第一个元素应该是string
// 参数数量错误
swapGeneric([1]);
// ❌ 错误:需要两个元素的元组
typescript
性能考虑
- 编译时特性:泛型不会增加运行时开销
- 代码生成:编译器会为每个具体类型组合生成优化后的代码
- 缓存机制:现代TypeScript编译器会重用相似的类型实现
单元测试建议
// 测试不同类型组合
expect(swapGeneric(["a", 1])).toEqual([1, "a"]);
expect(swapGeneric([true, null])).toEqual([null, true]);
// 测试类型保持
const result = swapGeneric(["x", 2]);
expect(typeof result[0]).toBe("number");
expect(typeof result[1]).toBe("string");
typescript
延伸思考
- 如何扩展支持三元组的交换?
function swap3<T, U, V>(tuple: [T, U, V]): [V, U, T] { return [tuple[2], tuple[1], tuple[0]]; }
typescript - 在React中的应用:
function SwapComponent<T, U>(props: { pair: [T, U] }) { const [a, b] = swapGeneric(props.pair); return <div>{a} - {b}</div>; }
typescript - 与条件类型的结合:
type Swapped<T> = T extends [infer A, infer B] ? [B, A] : never;
typescript
这个元组交换示例虽然简单,但生动展示了泛型在类型安全、代码复用和开发体验方面的强大优势。通过扩展应用,可以在各种场景中发挥类似的作用。
泛型高级应用
泛型约束 (extends)
泛型约束通过 extends
关键字限制泛型参数的类型范围,确保类型满足特定条件。
基础用法
interface Person {
name: string;
}
function logPerson<T extends Person>(person: T) {
console.log(person.name); // ✅ 确保person一定有name属性
}
logPerson({ name: "Alice", age: 25 }); // ✅ 兼容Person接口
logPerson("Bob"); // ❌ 错误:string不满足Person约束
typescript
多重约束
泛型可以同时继承多个接口:
interface Named {
name: string;
}
interface Aged {
age: number;
}
function logDetails<T extends Named & Aged>(obj: T) {
console.log(`${obj.name} is ${obj.age} years old`);
}
logDetails({ name: "Alice", age: 25 }); // ✅
logDetails({ name: "Bob" }); // ❌ 缺少age属性
typescript
结合类型操作
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key]; // 类型安全地访问属性
}
const user = { name: "Alice", age: 25 };
getProperty(user, "name"); // ✅ 返回string
getProperty(user, "age"); // ✅ 返回number
getProperty(user, "email"); // ❌ 错误:email不是user的属性
typescript
泛型应用场景
1. 泛型函数
特点:动态化参数和返回值类型。
示例:
function identity<T>(arg: T): T {
return arg;
}
// 使用
const num = identity<number>(42); // number
const str = identity("hello"); // 自动推断为string
typescript
2. 泛型接口
特点:接口成员类型可变。
示例:
interface ApiResponse<T> {
data: T;
status: number;
}
// 使用
const userResponse: ApiResponse<{ name: string }> = {
data: { name: "Alice" },
status: 200,
};
typescript
3. 泛型类
特点:类成员类型参数化。
示例:
class Queue<T> {
private items: T[] = [];
enqueue(item: T) {
this.items.push(item);
}
dequeue(): T | undefined {
return this.items.shift();
}
}
// 使用
const numberQueue = new Queue<number>();
numberQueue.enqueue(1);
numberQueue.enqueue(2);
typescript
4. 泛型约束
特点:限制泛型类型范围。
示例:
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}
merge({ name: "Alice" }, { age: 25 }); // ✅
merge("hello", 42); // ❌ 不满足object约束
typescript
实战技巧
1. 默认泛型参数
function fetchData<T = string>(url: string): Promise<T> {
return fetch(url).then(res => res.json());
}
// 使用
fetchData<{ id: number }>("/api/user"); // 显式指定
fetchData("/api/text"); // 默认为string
typescript
2. 条件类型与泛型
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<number>; // false
typescript
3. 泛型工具类型
TypeScript 内置工具类型(如 Partial
, Pick
)也是基于泛型:
type User = { name: string; age: number };
type PartialUser = Partial<User>; // { name?: string; age?: number }
typescript
常见问题解答
Q:泛型约束和类型断言有什么区别?
- 泛型约束:编译时强制类型满足条件。
- 类型断言:开发者手动告诉编译器类型信息(可能绕过检查)。
Q:泛型能用于React组件吗?
A:可以!例如:
interface Props<T> {
data: T;
renderItem: (item: T) => React.ReactNode;
}
function GenericList<T>({ data, renderItem }: Props<T>) {
return <div>{renderItem(data)}</div>;
}
typescript
Q:如何调试复杂的泛型代码?
A:使用 type
关键字定义中间类型,逐步检查:
type Step1<T> = T extends (...args: any[]) => infer R ? R : never;
typescript
延伸学习
- 官方文档:TypeScript Generics
- 高级技巧:映射类型、条件类型与泛型结合
- 实战项目:实现一个泛型缓存系统,支持任意数据类型存储 🚀
泛型是TypeScript最强大的特性之一,掌握后能大幅提升代码质量和开发效率!
泛型实践要点
1. 类型推导优先
TypeScript 的类型推导能力强大,在大多数情况下可以自动推断泛型类型,无需显式指定。
最佳实践:
// 不需要显式指定类型
const numArr = [1, 2, 3];
pushArr(numArr, 4); // 自动推断为 number
// 仅在复杂场景或需要明确类型时显式指定
pushArr<number | string>(mixedArr, "hello");
typescript
💡 何时需要显式指定?
- 函数重载时
- 类型推断不明确时(如联合类型)
- 需要限制类型范围时
2. 命名规范
泛型参数的命名应简洁且语义化,遵循通用约定:
参数名 | 典型用途 | 示例 |
---|---|---|
T | 通用类型(Type) | function identity<T>(arg: T) |
K | 对象键类型(Key) | function getValue<K extends keyof T> |
V | 对象值类型(Value) | interface Map<K, V> |
E | 数组元素类型(Element) | class Stack<E> |
反模式:
function badExample<SomeLongTypeName>(arg: SomeLongTypeName) {} // 过于冗长
typescript
3. 避免过度泛化
泛型的滥用会导致代码可读性降低。
适用场景:
✅ 需要支持多种数据类型的工具函数(如 Array.map
)
✅ 通用数据容器(如 List<T>
, Response<T>
)
不适用场景:
❌ 逻辑与类型强绑定的场景(如特定业务规则的校验函数)
❌ 类型永远固定的场景(如仅处理 string
的格式化函数)
示例:
// 过度泛化:不必要
function parseString<T extends string>(input: T): T {
return input.trim() as T;
}
// 更简单的实现
function parseString(input: string): string {
return input.trim();
}
typescript
4. 组合应用
泛型与接口、类、条件类型等结合,能实现高度复用的组件。
示例:泛型 + 接口
interface Repository<T> {
get(id: string): T;
save(entity: T): void;
}
class UserRepository implements Repository<User> {
get(id: string): User { /* ... */ }
save(user: User): void { /* ... */ }
}
typescript
示例:泛型 + 条件类型
type Nullable<T> = T | null;
type Promisify<T> = T extends Promise<infer U> ? T : Promise<T>;
typescript
代码重构实战
目标: 将重复的类型特定函数改为泛型函数。
重构前
function processNumbers(arr: number[]): number[] {
return arr.map(n => n * 2);
}
function processStrings(arr: string[]): string[] {
return arr.map(s => s.toUpperCase());
}
typescript
重构后
function processArray<T>(arr: T[], transform: (item: T) => T): T[] {
return arr.map(transform);
}
// 使用
const numbers = processArray([1, 2, 3], n => n * 2);
const strings = processArray(["a", "b"], s => s.toUpperCase());
typescript
关键改进:
- 逻辑复用:一套实现支持多种类型
- 灵活性:通过
transform
函数自定义处理逻辑 - 类型安全:编译器会检查
transform
的输入输出类型
延伸挑战
- 泛型工具函数:实现一个
filterByKey
函数,支持根据对象键过滤数组。function filterByKey<T, K extends keyof T>(arr: T[], key: K, value: T[K]): T[] { return arr.filter(item => item[key] === value); }
typescript - 泛型类:设计一个
Cache<T>
类,支持存储和检索任意类型的数据,并设置过期时间。 - 类型体操:用泛型 + 条件类型实现一个
DeepReadonly<T>
,递归地将对象所有属性设为只读。
总结
泛型的核心价值在于 平衡灵活性与类型安全。掌握以下原则:
✨ 能用推导就别写(减少冗余)
✨ 命名要短且准(T/K/V/E)
✨ 不要为了泛型而泛型(避免过度设计)
✨ 组合其他特性(接口、类、条件类型)
尝试在项目中找出 3 个可泛化的重复代码,动手重构吧! 🚀
↑