TypeScript泛型日常实用指南
你正在编写一个适用于任何类型的函数,但TypeScript总是抱怨any。或者你只是为了处理不同的类型而将同一个函数复制了三次。泛型解决了这个问题——它们允许你编写适用于多种类型的代码,同时保持类型安全。但许多开发者要么避免使用它们,要么过度使用。本指南将向你展示如何在日常代码中使用泛型,而不会感到头疼。
什么是泛型,为什么你应该关心?
泛型是一种参数化类型的方式。不是硬编码像string或number这样的类型,而是使用一个类型变量(通常是T),当函数或类被使用时,它会被替换为具体的类型。
为什么这很重要?因为它让你编写可重用的代码,同时保持类型安全。没有泛型,你要么复制代码,要么使用any,这违背了TypeScript的初衷。
泛型函数:构建块
让我们从一个简单的例子开始。假设你想要一个返回数组第一个元素的函数。
function firstElement(arr: any[]): any {
return arr[0];
}
这可以工作,但你丢失了类型信息。如果你传递一个字符串数组,返回类型是any,而不是string。现在使用泛型:
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
现在TypeScript知道,如果你传递一个string[],你会得到string | undefined。类型被保留了。
你也可以使用多个类型参数:
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
泛型接口和类型别名
泛型不仅限于函数。你可以创建泛型接口和类型别名来描述灵活的数据结构。
interface Box<T> {
value: T;
}
const stringBox: Box<string> = { value: "hello" };
const numberBox: Box<number> = { value: 42 };
这对于API响应特别有用。不需要为每个端点定义新的接口,你可以定义一个泛型ApiResponse<T>:
interface ApiResponse<T> {
data: T;
status: number;
}
type User = { id: number; name: string };
const response: ApiResponse<User> = {
data: { id: 1, name: "Alice" },
status: 200,
};
泛型约束:添加规则
有时你想限制可以与泛型一起使用的类型。这就是约束的用武之地。使用extends关键字来要求类型具有某些属性。
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
getLength("hello"); // OK
getLength([1, 2, 3]); // OK
getLength(42); // Error: number doesn't have length
约束使你的泛型更可预测,并防止运行时错误。
泛型类
类也可以是泛型的。这对于像栈、队列或仓库这样的数据结构很方便。
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
const top = numberStack.pop(); // number | undefined
何时使用泛型(何时不使用)
泛型很强大,但如果过度使用,它们会使代码更难阅读。这里有一个快速指南:
| 使用泛型当 | 避免泛型当 |
|---|---|
| 你需要保留类型关系 | 简单的联合类型或any就足够了 |
| 你正在编写可重用的工具 | 类型总是相同的 |
| 你想避免代码重复 | 它使代码更难理解 |
一个好的经验法则:如果你发现自己为多种类型编写相同的逻辑,考虑使用泛型。如果你添加了第三个类型参数,而没有人能理解它,那就退一步。
常见模式和最佳实践
- 为类型参数使用描述性名称。
T对于简单情况没问题,但TData或TResponse可能更清晰。 - 优先使用约束而不是
any。 如果你需要灵活性,使用extends来强制最小形状。 - 利用默认类型参数。 你可以提供一个默认值:
<T = string>。 - 将泛型与工具类型一起使用。 TypeScript内置的
Partial<T>、Readonly<T>和Record<K, V>都是泛型的。
真实世界示例:泛型API客户端
让我们用一个实际例子来总结:一个简单的API客户端,它获取数据并返回类型化的响应。
async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json() as Promise<T>;
}
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
async function main() {
const post = await fetchData<Post>("https://jsonplaceholder.typicode.com/posts/1");
console.log(post.title); // string
}
这种模式在前端代码中很常见。你获得了类型安全,而不必为每个端点重复自己。
常见问题
TypeScript中的<T>语法是什么意思?
<T>声明了一个类型参数。它是一个占位符,用于在函数、接口或类被使用时指定的类型。你可以给它起任何名字,但T是单个类型的惯例。
我可以使用多个泛型类型参数吗?
是的。你可以根据需要定义任意多个,用逗号分隔:<T, U, V>。使用描述性名称来保持代码可读性。
如何将泛型约束为只接受对象?
使用T extends object。这确保T是一个对象类型,而不是像string或number这样的原始类型。你也可以约束为特定的形状,比如T extends { id: number }。
泛型是编写可维护TypeScript的核心部分。如果你正在处理JSON数据并想快速验证或格式化它,试试我们的JSON Formatter来美化打印和检查你的API响应。