TypeScript 泛型
最后修改于 2025 年 3 月 5 日
TypeScript 中的泛型能够创建可重用、类型安全组件。它们允许您定义适用于多种类型的函数、类和接口。本教程将通过实际示例探讨泛型。
泛型是类型的占位符。它们允许您编写灵活且可重用的代码,而不会牺牲类型安全。泛型使用尖括号(<T>)定义。
基本泛型函数
此示例展示了一个返回输入值的简单泛型函数。
function identity<T>(arg: T): T {
return arg;
}
console.log(identity<number>(42)); // Output: 42
console.log(identity<string>("Hello")); // Output: Hello
在此示例中,identity 函数使用泛型类型 T,使其能够接受并返回在调用站点指定的任何类型。通过在尖括号中显式提供 number 或 string,我们为每次调用定义 T 所代表的含义。TypeScript 通过匹配参数和返回类型来确保类型安全,输出显示的值与输入值相同。这说明了泛型如何在不丢失编译时检查的情况下实现类型灵活性。
泛型数组
泛型可与数组一起使用以确保类型安全。此示例演示了一个处理数组的泛型函数。
function reverseArray<T>(arr: T[]): T[] {
return arr.reverse();
}
console.log(reverseArray<number>([1, 2, 3])); // Output: [3, 2, 1]
console.log(reverseArray<string>(["a", "b", "c"])); // Output: ["c", "b", "a"]
reverseArray 函数使用泛型类型 T 来指定输入数组 T[] 中元素的类型,并返回相同类型的数组。当使用数字数组或字符串数组调用时,TypeScript 会强制所有元素都匹配指定的类型 T。应用 reverse() 方法后,输出显示了反转后的数组。这说明了泛型如何跨数组操作保持类型一致性。
泛型接口
接口也可以使用泛型。此示例为键值对定义了一个泛型接口。
interface KeyValuePair<K, V> {
key: K;
value: V;
}
const pair: KeyValuePair<string, number> = { key: "age", value: 30 };
console.log(pair); // Output: { key: "age", value: 30 }
KeyValuePair 接口使用两种泛型类型,K 代表键,V 代表值,使其可用于任何键值组合。在此实例中,将 string 分配给 K,将 number 分配给 V,从而为 pair 对象创建特定类型。TypeScript 确保对象符合此结构,输出显示了定义的键值对。这表明泛型接口如何提供可重用的类型定义。
泛型类
类可以使用泛型来创建可重用组件。此示例展示了一个泛型堆栈类。
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);
console.log(numberStack.pop()); // Output: 2
Stack 类使用泛型类型 T 来定义它可以容纳的项的类型。items 数组的类型为 T[],并且 push 和 pop 等方法在此类型上运行。当实例化为 Stack<number> 时,它只接受数字,从而确保类型安全。该示例推入两个数字并弹出最后一个,输出显示 2。这突出显示了泛型类如何实现类型特定的可重用数据结构。
泛型约束
约束限制了可与泛型一起使用的类型。此示例确保泛型类型具有 length 属性。
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): void {
console.log(arg.length);
}
logLength("Hello"); // Output: 5
logLength([1, 2, 3]); // Output: 3
logLength 函数使用泛型类型 T,并受 extends Lengthwise 约束,这意味着 T 必须具有类型为 number 的 length 属性。这使得该函数可以处理字符串和数组(两者都有 length),但不能处理不兼容的类型(如数字)。输出显示了字符串(5)和数组(3)的长度,展示了约束如何确保类型兼容性同时保持灵活性。
泛型实用工具类型
TypeScript 提供了内置的实用工具类型,如 Partial 和 Readonly。此示例演示了它们的用法。
interface User {
name: string;
age: number;
}
const partialUser: Partial<User> = { name: "John" };
const readonlyUser: Readonly<User> = { name: "Jane", age: 25 };
console.log(partialUser); // Output: { name: "John" }
console.log(readonlyUser); // Output: { name: "Jane", age: 25 }
此示例使用 TypeScript 的内置泛型实用工具类型。Partial<User> 使 User 的所有属性都成为可选的,因此 partialUser 可以省略 age。Readonly<User> 使所有属性成为只读的,防止在初始化 readonlyUser 后进行修改。TypeScript 在编译时强制执行这些约束,输出显示了结果对象。这展示了实用工具类型如何简化常见的类型转换。
具有多种类型的泛型函数
泛型可以处理多种类型。此示例展示了一个组合两种不同类型值的函数。
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
const result = merge({ name: "Alice" }, { age: 30 });
console.log(result); // Output: { name: "Alice", age: 30 }
merge 函数使用两种泛型类型 T 和 U 来表示两个输入对象的类型。它使用展开运算符返回一个交集类型 T & U,合并它们的属性。在这种情况下,T 被推断为 { name: string },U 被推断为 { age: number },从而生成一个合并的对象。输出显示了合并的属性,说明了多种泛型类型如何实现灵活的对象组合。
泛型默认类型
泛型可以具有默认类型。此示例定义了一个带有默认类型的泛型函数。
function createArray<T = string>(length: number, value: T): T[] {
return Array(length).fill(value);
}
console.log(createArray(3, "a")); // Output: ["a", "a", "a"]
console.log(createArray<number>(3, 1)); // Output: [1, 1, 1]
createArray 函数使用泛型类型 T,其默认值为 string。如果未指定类型,T 将默认为 string,如第一次调用所示。当在第二次调用中显式设置为 number 时,它会覆盖默认值。该函数创建指定长度并填充给定值的数组,输出显示了这两种情况。这说明了当期望常见类型时,默认类型如何提高可用性。
泛型类型别名
类型别名可以是泛型的,从而提供了一种定义可重用类型模式的方法。此示例展示了一个结果类型的泛型类型别名。
type Result<T> = { success: true; value: T } | { success: false; error: string };
const successResult: Result<number> = { success: true, value: 42 };
const errorResult: Result<string> = { success: false, error: "Not found" };
console.log(successResult); // Output: { success: true, value: 42 }
console.log(errorResult); // Output: { success: false, error: "Not found" }
Result 类型别名使用泛型类型 T 来定义成功和错误情况的联合。对于 successResult,T 为 number,表示具有值的成功结果。对于 errorResult,T 为 string,但错误情况使用固定的 error 属性。TypeScript 确保每个对象都符合联合的一种形状,输出显示了两种可能的状态。这说明了泛型类型别名如何创建灵活、可重用的类型定义。
泛型条件类型
条件类型允许泛型根据条件进行适应。此示例提取函数的返回类型。
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function greet(): string {
return "Hello";
}
type GreetReturn = ReturnType<typeof greet>;
const message: GreetReturn = "Hello";
console.log(message); // Output: Hello
ReturnType 泛型类型使用条件类型和 infer R 来提取函数的返回类型。如果 T 是一个函数,它会推断 R 为返回类型;否则为 never。将其应用于 greet 函数(通过 typeof greet),它解析为 string。因此,GreetReturn 类型为 string,允许将 message 分配给 "Hello"。输出确认了这一点,显示了条件类型如何实现动态类型推断。
泛型工厂函数
工厂函数可以使用泛型来创建不同类型的实例。此示例根据构造函数创建对象。
class Animal {
constructor(public name: string) {}
}
class Car {
constructor(public model: string) {}
}
function createInstance<T>(ctor: new (arg: string) => T, arg: string): T {
return new ctor(arg);
}
const dog = createInstance(Animal, "Dog");
const sedan = createInstance(Car, "Sedan");
console.log(dog.name); // Output: Dog
console.log(sedan.model); // Output: Sedan
createInstance 函数使用泛型类型 T,并接受一个构造函数(new (arg: string) => T)和一个参数。它使用提供的构造函数创建 T 的实例。这里,Animal 和 Car 类使用特定参数进行了实例化,TypeScript 确保返回的实例与预期的类型匹配。输出显示了创建对象的属性,说明了泛型工厂函数如何支持类型安全的.*
最佳实践
- 明智地使用泛型:仅当它们增强类型安全或代码可重用性时才使用泛型,在特定类型就足够的简单场景中避免不必要的复杂性。
- 清晰地记录泛型类型:包含详细的注释或类型注解,以解释泛型参数的目的和约束,特别是对于复杂或嵌套的泛型类型。
- 有效应用约束:使用类型约束(例如
extends)将泛型类型限制为满足特定要求的类型,以确保兼容性并减少运行时错误。 - 用多种类型进行测试:使用各种类型(例如,原始类型、对象、数组)彻底测试泛型函数、类和接口,以验证灵活性和正确性。
- 为类型参数使用特定名称:当涉及多个类型参数时,使用诸如
TKey或TValue之类的描述性名称,而不是通用的T,以提高可读性。 - 避免过于宽泛的泛型:尽可能避免使用无界泛型(例如,没有约束的
<T>),因为它们会削弱类型安全性并导致意外行为。 - 利用实用工具类型:利用内置的泛型实用工具类型,如
Partial、Pick或ReturnType,以简化常见模式并减少样板代码。 - 将泛型与接口结合使用:将泛型与接口配对以定义可重用的契约,确保不同实现之间的一致结构,同时保持类型安全。
来源
本教程通过实际示例介绍了 TypeScript 泛型。使用泛型来编写灵活、可重用且类型安全的.*