日常代码中的 TypeScript 泛型
你正在编写一个返回数组第一个元素的函数。你希望它能处理数字、字符串和对象,但你也希望返回类型与输入类型匹配。没有泛型,你只能求助于 any 并失去所有类型安全。这就是 TypeScript 泛型的用武之地。
泛型让你编写能处理多种类型的代码,同时保留具体的类型信息。在本指南中,我们将探讨在日常 TypeScript 代码中使用泛型的实用模式。
什么是泛型?
泛型是一种参数化类型的方式。就像函数接受参数一样,泛型接受类型参数。它们允许你创建可复用的组件,这些组件可以处理多种类型,而不是单一类型。
考虑这个恒等函数:
function identity<T>(arg: T): T {
return arg;
}
这里,T 是一个类型变量。当你调用 identity(42) 时,TypeScript 推断 T 为 number。当你调用 identity("hello") 时,T 变为 string。返回类型始终与输入类型匹配。
实践中的泛型函数
让我们看一个常见场景:一个将值包装在对象中的函数。
function wrapInObject<T>(value: T): { value: T } {
return { value };
}
没有泛型,你不得不使用 any 或 unknown,调用者会丢失类型信息。有了泛型,返回类型精确为 { value: T }。
另一个日常示例是返回数组最后一个元素的函数:
function last<T>(arr: T[]): T | undefined {
return arr[arr.length - 1];
}
TypeScript 从数组参数推断 T,所以 last([1, 2, 3]) 返回 number | undefined。
泛型约束
有时你想限制可用于泛型的类型。例如,你可能需要访问仅存在于某些类型上的属性。你可以使用 extends 关键字添加约束。
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
现在 T 必须有一个类型为 number 的 length 属性。这适用于字符串、数组以及任何具有 length 属性的对象。
你也可以对接口使用约束:
interface HasId {
id: string;
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
这确保传递给 findById 的项具有 id 属性。
泛型接口和类型别名
泛型不仅限于函数。你可以定义泛型接口和类型别名来创建灵活的数据结构。
interface ApiResponse<T> {
data: T;
status: number;
error?: string;
}
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(url);
const data = await response.json();
return { data, status: response.status };
}
现在你可以精确地类型化 API 响应:
interface User {
id: number;
name: string;
}
const userResponse = await fetchData<User>('/api/user');
// userResponse.data is User
泛型类
类也可以是泛型的。一个常见示例是简单的栈:
class Stack<T> {
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 Stack<number>();
numberStack.push(1);
const num = numberStack.pop(); // number | undefined
React 组件中的泛型
如果你正在构建 React 组件,泛型可以让你的组件更可复用。例如,一个渲染任意类型项的列表组件:
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map((item, index) => <li key={index}>{renderItem(item)}</li>)}</ul>;
}
现在你可以将 List 与任何类型一起使用,并在 renderItem 回调上获得完整的类型检查。
常见陷阱和最佳实践
- 不要过度使用泛型。 如果函数只适用于特定类型,就直接使用该类型。泛型会增加复杂性。
- 使用描述性的类型参数名称。 虽然
T很常见,但对于多个参数,使用像TInput、TOutput这样的名称。 - 优先使用约束而不是
any。 约束在保持灵活性的同时提供类型安全。 - 记住泛型在运行时被擦除。 它们只存在于编译时。
比较:泛型 vs. any vs. 联合类型
| 方法 | 类型安全 | 灵活性 | 使用场景 |
|---|---|---|---|
any |
无 | 高 | 迁移 JS、快速原型 |
| 联合类型 | 好 | 限于已知类型 | 当你知道所有可能的类型时 |
| 泛型 | 强 | 高 | 保留类型的可复用组件 |
常见问题
什么时候应该使用泛型而不是 any?
每当你想在函数或组件中保留类型信息时,就使用泛型。any 禁用类型检查,而泛型保持它。如果你发现自己为了让它工作而使用 any,考虑一下泛型是否能更安全地解决它。
我可以使用多个类型参数吗?
是的。你可以定义带有多个类型参数的函数,例如 function pair<K, V>(key: K, value: V): [K, V]。当有多个参数时,使用描述性名称。
泛型在 .tsx 文件中如何与箭头函数一起工作?
在 .tsx 文件中,<T> 可能与 JSX 产生歧义。使用尾随逗号:const identity = <T,>(arg: T): T => arg; 或使用 extends unknown:<T extends unknown>。
泛型是一个强大的功能,可以让你的 TypeScript 代码更可复用和类型安全。从简单的泛型函数开始,然后逐渐将它们应用到接口和类。如果你需要格式化或验证 JSON 数据并保证类型安全,请查看我们的 JSON Formatter 工具来快速检查和美化打印你的 JSON。