前言
泛型是 TypeScript 中最为强大的特性之一,它为我们提供了创建可重用、类型安全的代码组件的能力。本文将全面地介绍 TypeScript 泛型的各个方面。
一、泛型的基本概念
1.1 什么是泛型?
泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。这种特性可以极大地增加代码的灵活性和可重用性。
// 不使用泛型的例子 - 只能返回数字类型
function identityNumber(arg: number): number {
return arg;
}
// 使用泛型的例子 - 可以返回任何类型
function identity<T>(arg: T): T {
return arg;
}
// 使用示例
let output1 = identity<string>("myString"); // 类型为 string
let output2 = identity<number>(100); // 类型为 number
1.2 为什么需要泛型?
在没有泛型的情况下,开发者通常面临两种选择:
- 为每种类型编写重复代码
- 使用
any
类型(失去类型安全性)
泛型完美解决了这一困境,实现了"一次编写,多类型使用"的目标,同时保持类型安全。
二、泛型的基本用法
2.1 泛型函数
泛型函数是最基本的泛型应用形式:
function logAndReturn<T>(value: T): T {
console.log(value);
return value;
}
// 使用
const stringResult = logAndReturn<string>("Hello");
const numberResult = logAndReturn<number>(42);
TypeScript 的类型推断机制使得我们通常可以省略显式的类型参数:
const inferredString = logAndReturn("Hello"); // 推断为string
const inferredNumber = logAndReturn(42); // 推断为number
2.2 泛型接口
泛型接口允许我们定义可以适应多种类型的接口:
interface KeyValuePair<K, V> {
key: K;
value: V;
}
// 使用
let pair1: KeyValuePair<number, string> = { key: 1, value: "Apple" };
let pair2: KeyValuePair<string, boolean> = { key: "isAvailable", value: true };
2.3 泛型类
泛型类使得类可以处理多种数据类型:
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
constructor(zeroValue: T, add: (x: T, y: T) => T) {
this.zeroValue = zeroValue;
this.add = add;
}
}
// 使用
let myNumber = new GenericNumber<number>(0, (x, y) => x + y);
console.log(myNumber.add(5, 10)); // 15
let myString = new GenericNumber<string>("", (x, y) => x + y);
console.log(myString.add("Hello", " World")); // "Hello World"
2.4 泛型标识符
2.4.1 通用类型标识符
T
(Type) - 最通用的类型参数
// 基本泛型函数
function identity<T>(arg: T): T {
return arg;
}
const result = identity<string>("Hello"); // result 类型为 string
const inferred = identity(42); // inferred 类型为 number
2.4.2 集合类标识符
E
(Element) - 集合元素类型
// 数组处理函数
function firstElement<E>(arr: E[]): E | undefined {
return arr[0];
}
const num = firstElement([1, 2, 3]); // num 类型为 number | undefined
const str = firstElement(["a", "b"]); // str 类型为 string | undefined
2.4.3. 键值相关标识符
K
(Key) - 对象键类型
// 获取对象所有键的类型
function getKeys<K extends string>(obj: Record<K, any>): K[] {
return Object.keys(obj) as K[];
}
const keys = getKeys({ name: "Alice", age: 30 }); // keys 类型为 ("name" | "age")[]
V
(Value) - 对象值类型
// 反转键值对
function invert<K extends string, V extends string>(obj: Record<K, V>): Record<V, K> {
return Object.fromEntries(
Object.entries(obj).map(([k, v]) => [v, k])
) as Record<V, K>;
}
const inverted = invert({ a: "x", b: "y" }); // 类型为 { x: "a", y: "b" }
2.4.4 函数相关标识符
R
(Return) - 函数返回类型
// 包装函数返回值
function wrapResult<R>(fn: () => R): { result: R } {
return { result: fn() };
}
const wrapped = wrapResult(() => "Hello"); // wrapped 类型为 { result: string }
A
(Argument)- 函数参数类型
// 函数参数记录器
function logArgument<A>(arg: A): A {
console.log("Argument:", arg);
return arg;
}
const logged = logArgument(123); // logged 类型为 number
U
- 第二个通用类型参数
// 合并两个不同类型的值
function mergeValues<T, U>(first: T, second: U): T & U {
return { ...first, ...second };
}
const merged = mergeValues(
{ name: "Alice" },
{ age: 30 }
); // 类型为 { name: string } & { age: number }
2.4.5 描述性标识符示例
TInput
和 TOutput
- 输入输出类型
// 数据转换函数
function transform<TInput, TOutput>(
input: TInput,
converter: (input: TInput) => TOutput
): TOutput {
return converter(input);
}
const output = transform("123", (s) => parseInt(s)); // output 类型为 number
用法总结
- 简单场景:优先使用单字母标识符 (T, K, V 等)
- 复杂场景:使用描述性标识符提高可读性 (TResult, TInput 等)
- 多参数:按顺序使用 T, U, V 或更具描述性的名称
- 一致性:在项目中保持命名风格一致
- 文档注释:为复杂泛型添加注释说明类型参数的用途
标识符 | 典型使用场景 | 示例 |
---|---|---|
T | 基础类型(Type) | Array<T> |
K | 键类型(Key) | Record<K,V> |
V | 值类型(Value) | Map<K,V> |
E | 元素/事件类型(Element) | Event<E> |
R | 返回类型(Return) | Promise<R> |
特殊场景处理
-
多个泛型参数时建议保持字母顺序关系:
function convert<T, U extends T>(input: T): U {...}
-
嵌套泛型使用层级命名:
type Nested<T, U extends keyof T> = { [K in U]: T[K][]; }
三、泛型约束
3.1 基本约束
有时我们需要限制泛型的类型范围,这时可以使用泛型约束:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // 现在我们知道 arg 有 .length 属性
return arg;
}
// 使用
loggingIdentity("hello"); // OK
loggingIdentity([1, 2, 3]); // OK
loggingIdentity(3); // Error: number 没有 .length 属性
3.2 多重约束
泛型参数可以同时继承多个类型:
interface Printable {
print(): void;
}
interface Loggable {
log(): void;
}
function processItem<T extends Printable & Loggable>(item: T): void {
item.print();
item.log();
}
3.3 使用类型参数约束
我们可以使用keyof
关键字来约束类型参数必须是另一个类型的属性名:
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3 };
getProperty(x, "a"); // OK
getProperty(x, "d"); // Error: 参数类型 '"d"' 不能赋值给参数类型 '"a" | "b" | "c"'
四、高级泛型特性
4.1 泛型默认类型
泛型默认类型的语法类似于函数参数的默认值:
基本语法
<T = DefaultType>
泛型函数中的默认类型
function createArray<T = string>(length: number, value: T): T[] {
return Array(length).fill(value);
}
// 使用默认类型
const strArray = createArray(3, "x"); // T 推断为 string
console.log(strArray); // ["x", "x", "x"]
// 显式指定类型
const numArray = createArray<number>(3, 5);
console.log(numArray); // [5, 5, 5]
泛型接口中的默认类型
interface KeyValuePair<K = string, V = number> {
key: K;
value: V;
}
// 使用默认类型
const pair1: KeyValuePair = { key: "age", value: 30 };
// 覆盖部分默认类型
const pair2: KeyValuePair<number> = { key: 1, value: 100 };
// 完全覆盖默认类型
const pair3: KeyValuePair<boolean, string> = { key: true, value: "yes" };
泛型类中的默认类型
class Container<T = any> {
constructor(private content: T) {}
getContent(): T {
return this.content;
}
}
// 使用默认类型any
const anyContainer = new Container("可以是任何类型");
console.log(anyContainer.getContent());
// 指定具体类型
const stringContainer = new Container<string>("必须是字符串");
console.log(stringContainer.getContent());
4.2 条件类型
条件类型允许我们根据条件选择类型:
基本语法
T extends U ? X : Y
这个类型表达式表示:如果类型 T 可以赋值给类型 U,则结果类型为 X,否则为 Y。
示例
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"
4.3 映射类型
映射类型是 TypeScript 中强大的泛型特性,它允许我们基于现有类型创建新类型,通过转换现有类型的属性来生成新的类型结构。映射类型极大地增强了 TypeScript 的类型系统表达能力,让我们能够以编程方式操作和转换类型。
(映射类型允许我们基于旧类型创建新类型)
基本语法
{ [P in K]: T }
其中:
- K 是一个可迭代的类型(通常是 keyof T)
- P 是每次迭代中当前的属性名
- T 是属性的类型(可以基于 P 进行计算)
示例
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Partial<T> = {
[P in keyof T]?: T[P];
};
// 使用
interface Person {
name: string;
age: number;
}
type ReadonlyPerson = Readonly<Person>;
type PartialPerson = Partial<Person>;
五、泛型用法总结
-
命名约定:
- 通常使用单个大写字母作为类型变量名(如 T、K、V 等)
- 有明确含义时可以使用描述性名称(如 TKey、TValue)
-
适度使用:
- 不是所有情况都需要泛型,只在需要灵活性时使用
- 避免过度复杂的泛型类型
-
明确约束:
- 尽可能使用约束来限制类型参数的范围
- 使用
extends
关键字明确类型要求
-
文档注释:
- 为复杂的泛型类型添加详细的文档注释
- 使用
@typeparam
标注泛型参数
/**
* 将两个对象合并
* @typeparam T - 第一个对象的类型
* @typeparam U - 第二个对象的类型
* @param obj1 - 第一个对象
* @param obj2 - 第二个对象
* @returns 合并后的对象
*/
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
六、总结
TypeScript 泛型是一个强大的工具,它可以帮助我们:
- 编写更灵活、可重用的代码
- 保持类型安全,减少运行时错误
- 创建更抽象的组件和数据结构
- 提高代码的可读性和可维护性
从简单的泛型函数到复杂的条件类型和映射类型,泛型为 TypeScript 的类型系统提供了极大的表现力。通过合理应用泛型,我们可以构建出既灵活又类型安全的代码库,大大提高开发效率和代码质量。
掌握泛型需要实践和耐心,但一旦掌握,它将极大地提升你的 TypeScript 编程能力。建议从简单的泛型函数开始,逐步尝试更复杂的泛型应用场景。