
1. 泛型是什么为什么TypeScript需要它第一次接触TypeScript泛型时我正被一个棘手的问题困扰着如何在不重复代码的情况下让一个函数能够处理多种类型的数据比如一个简单的identity函数既要能返回number又要能返回string。如果不用泛型我们可能会写出这样的代码function identityNumber(arg: number): number { return arg; } function identityString(arg: string): string { return arg; }这显然违反了DRYDont Repeat Yourself原则。而泛型的出现完美解决了这个问题function identityT(arg: T): T { return arg; }这个T就是类型参数它就像一个占位符在函数被调用时会被具体的类型替换。这种参数化类型的能力正是泛型的核心价值。注意泛型参数通常用单个大写字母表示常见的有TType、KKey、VValue等但这只是约定而非强制要求。2. 泛型基础从简单示例到高级应用2.1 泛型函数让我们从一个更实用的例子开始 - 一个获取数组最后一个元素的函数function lastElementT(arr: T[]): T | undefined { return arr[arr.length - 1]; } const nums [1, 2, 3]; const lastNum lastElement(nums); // 类型推断为number const strs [a, b, c]; const lastStr lastElement(strs); // 类型推断为string这里TypeScript会自动推断出T的类型无需显式指定。但有时我们需要明确指定类型参数const mixed [1, a, true]; const last lastElementstring | number | boolean(mixed);2.2 泛型接口泛型同样适用于接口定义。比如一个通用的响应接口interface ApiResponseT { code: number; message: string; data: T; } // 用户数据响应 const userResponse: ApiResponse{ id: number; name: string } { code: 200, message: success, data: { id: 1, name: Alice } }; // 产品列表响应 const productResponse: ApiResponseArray{ id: number; price: number } { code: 200, message: success, data: [{ id: 1, price: 100 }, { id: 2, price: 200 }] };2.3 泛型类在类中使用泛型可以创建高度可复用的组件。比如一个简单的栈实现class StackT { private items: T[] []; push(item: T): void { this.items.push(item); } pop(): T | undefined { return this.items.pop(); } peek(): T | undefined { return this.items[this.items.length - 1]; } } // 使用 const numberStack new Stacknumber(); numberStack.push(1); numberStack.push(2); console.log(numberStack.pop()); // 2 const stringStack new Stackstring(); stringStack.push(hello); stringStack.push(world); console.log(stringStack.pop()); // world3. 泛型约束给泛型戴上紧箍咒有时候我们希望泛型参数有一定的限制而不是接受任何类型。这就是泛型约束的作用。3.1 基本约束假设我们想写一个获取对象属性长度的函数function getLengthT extends { length: number }(obj: T): number { return obj.length; } console.log(getLength(hello)); // 5 console.log(getLength([1, 2, 3])); // 3 console.log(getLength({ length: 10 })); // 10 // console.log(getLength(123)); // 错误number没有length属性这里的T extends { length: number }表示T必须包含length属性。3.2 多重约束泛型可以同时继承多个类型interface Named { name: string; } interface Aged { age: number; } function logPersonT extends Named Aged(person: T): void { console.log(${person.name} is ${person.age} years old); } logPerson({ name: Alice, age: 30 }); // OK // logPerson({ name: Bob }); // 错误缺少age属性3.3 keyof约束一个常见的模式是使用keyof来确保我们访问的是对象上存在的属性function getPropertyT, K extends keyof T(obj: T, key: K): T[K] { return obj[key]; } const person { name: Alice, age: 30 }; console.log(getProperty(person, name)); // Alice // console.log(getProperty(person, gender)); // 错误gender不在person的键中4. 高级泛型技巧4.1 条件类型TypeScript的条件类型允许我们根据条件选择类型type IsStringT T extends string ? yes : no; type A IsStringstring; // yes type B IsStringnumber; // no更实用的例子是NonNullable类型它可以从联合类型中移除null和undefinedtype NonNullableT T extends null | undefined ? never : T; type C NonNullablestring | null | undefined; // string4.2 映射类型映射类型允许我们基于旧类型创建新类型type ReadonlyT { readonly [P in keyof T]: T[P]; }; type PartialT { [P in keyof T]?: T[P]; }; interface Person { name: string; age: number; } type ReadonlyPerson ReadonlyPerson; /* { readonly name: string; readonly age: number; } */ type PartialPerson PartialPerson; /* { name?: string; age?: number; } */4.3 类型推断inferinfer关键字允许我们在条件类型中声明一个类型变量type ReturnTypeT T extends (...args: any[]) infer R ? R : never; function foo(): number { return 42; } type FooReturn ReturnTypetypeof foo; // number5. 泛型在React中的应用在React组件开发中泛型能提供更好的类型安全。比如一个通用的列表组件interface ListPropsT { items: T[]; renderItem: (item: T) React.ReactNode; } function ListT({ items, renderItem }: ListPropsT) { return ( ul {items.map((item, index) ( li key{index}{renderItem(item)}/li ))} /ul ); } // 使用 const users [{ id: 1, name: Alice }, { id: 2, name: Bob }]; function UserList() { return ( List items{users} renderItem{(user) ( div span{user.id}/span span{user.name}/span /div )} / ); }6. 常见问题与解决方案6.1 泛型与any的区别新手常问既然any可以表示任何类型为什么还要用泛型关键区别在于类型安全function identityAny(arg: any): any { return arg; } let outputAny identityAny(hello); outputAny.toFixed(); // 运行时错误但编译时不会报错 function identityGenericT(arg: T): T { return arg; } let outputGeneric identityGeneric(hello); // outputGeneric.toFixed(); // 编译时报错string没有toFixed方法6.2 泛型默认值可以为泛型参数指定默认类型interface PaginationT any { current: number; pageSize: number; total: number; data: T[]; } // 使用默认类型 const pagination1: Pagination { current: 1, pageSize: 10, total: 100, data: [1, 2, 3] }; // 指定类型 const pagination2: Pagination{ id: number } { current: 1, pageSize: 10, total: 100, data: [{ id: 1 }, { id: 2 }] };6.3 泛型与函数重载有时泛型可以与函数重载结合使用function processT extends string | number(input: T): T extends string ? string : number; function process(input: any): any { if (typeof input string) { return input.toUpperCase(); } else { return input.toFixed(2); } } const strResult process(hello); // string const numResult process(3.14159); // number7. 性能考量与最佳实践虽然泛型很强大但过度使用可能会影响编译性能和代码可读性。以下是一些建议避免过度嵌套多层嵌套的泛型会使类型难以理解// 不推荐 type DeepNestedT { data: { items: Array{ value: T; metadata: Mapstring, T; }; }; };合理使用类型别名为复杂泛型类型创建别名// 推荐 type UserDataT { user: T; timestamp: Date; }; const data: UserData{ id: number; name: string } ...;优先使用类型推断让TypeScript自动推断类型参数// 推荐让TS推断 const result identity(hello); // 不必要 const result identitystring(hello);文档化复杂泛型为复杂的泛型类型和函数添加注释/** * 将对象数组转换为键值对映射 * typeparam T 数组元素类型 * typeparam K 用作键的属性名 */ function arrayToMapT, K extends keyof T(items: T[], key: K): Recordstring, T { return items.reduce((acc, item) { acc[String(item[key])] item; return acc; }, {} as Recordstring, T); }在实际项目中我发现合理使用泛型可以显著提高代码的可维护性和类型安全性。特别是在构建可复用的工具函数和组件时泛型几乎不可或缺。