TypeScript 中的泛型是用于编写可重用的、灵活的代码的一种重要工具。泛型允许你在类、接口、函数等中定义类型参数,使得你能够在类型层面上进行抽象,进而提高代码的灵活性和可重用性。使用泛型时,你可以通过类型约束来确保类型安全。以下是泛型在 TypeScript 中的常见用法及实践场景。
1. 基本的泛型用法
泛型可以应用于函数、类和接口。最常见的用途是定义函数,允许函数接收不同类型的参数,并根据参数类型返回相应的结果。
函数中的泛型
// 泛型函数
function identity<T>(value: T): T {
return value;
}
// 使用泛型函数
let result1 = identity(5); // result1 类型为 number
let result2 = identity("hello"); // result2 类型为 string
在这个例子中,identity 函数是一个泛型函数,它接受一个类型为 T 的参数,并返回类型为 T 的结果。T 是类型变量,在调用 identity 时会根据传入的参数类型来推断。
类中的泛型
泛型也可以用于类,允许类的实例具有灵活的类型。
class Box<T> {
value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
}
const numberBox = new Box(123); // numberBox 类型为 Box<number>
const stringBox = new Box("hello"); // stringBox 类型为 Box<string>
console.log(numberBox.getValue()); // 输出: 123
console.log(stringBox.getValue()); // 输出: hello
在这里,Box 是一个泛型类,类型 T 在创建实例时被指定,getValue 方法返回该类型的值。
2. 泛型约束
通过使用泛型约束,可以限制泛型的类型范围,确保传入的类型符合特定的结构或条件。常见的约束方式包括使用 extends 关键字来指定类型约束。
数字类型的约束
function add<T extends number>(a: T, b: T): T {
return a + b;
}
let sum = add(10, 20); // sum 类型为 number
// add("10", "20"); // 错误: 类型 "string" 不满足类型 "number"
在这个例子中,T extends number 表示泛型 T 必须是 number 类型,这确保了函数 add 只接受数字类型的参数。
对象类型的约束
interface Person {
name: string;
age: number;
}
function greet<T extends Person>(person: T): string {
return `Hello, ${person.name}`;
}
const person = { name: "Alice", age: 30 };
console.log(greet(person)); // 输出: Hello, Alice
在这里,泛型 T 被约束为 Person 类型或其子类型,因此 greet 函数只能接受符合 Person 类型结构的对象。
3. 使用泛型进行组合
泛型的强大之处在于可以组合使用,从而增强代码的灵活性。可以将泛型与接口、类型别名等结合,进行更复杂的数据结构设计。
泛型接口
interface Pair<T, U> {
first: T;
second: U;
}
const pair: Pair<number, string> = { first: 1, second: "hello" };
在这个例子中,Pair 是一个泛型接口,它有两个类型参数 T 和 U,分别表示 first 和 second 的类型。实例 pair 被指定为 Pair<number, string>。
泛型与函数式编程结合
function map<T, U>(arr: T[], transform: (item: T) => U): U[] {
return arr.map(transform);
}
const numbers = [1, 2, 3];
const strings = map(numbers, num => num.toString()); // strings 类型为 string[]
map 是一个泛型函数,它接受一个类型为 T 的数组,并且接受一个转换函数 transform,该函数将数组中的每个元素转换为类型 U 的元素。返回的结果是一个 U[] 类型的数组。
4. 默认泛型类型
TypeScript 允许为泛型提供默认类型,这样在调用时,如果没有指定类型,TypeScript 会自动使用默认类型。
function createArray<T = number>(length: number, value: T): T[] {
return new Array(length).fill(value);
}
let arr1 = createArray(3, 5); // arr1 类型为 number[]
let arr2 = createArray(3, "hello"); // arr2 类型为 string[]
在这里,createArray 函数的泛型类型 T 默认值是 number,如果调用时没有指定类型,TypeScript 会默认使用 number 类型。
5. 泛型在复杂场景中的应用
泛型与联合类型
泛型可以和联合类型结合使用,从而创建更加灵活的类型约束。
function combine<T, U>(a: T, b: U): T | U {
return a;
}
const result1 = combine(1, "hello"); // result1 类型为 number | string
const result2 = combine(true, [1, 2, 3]); // result2 类型为 boolean | number[]
泛型与递归类型
TypeScript 中的泛型还可以支持递归类型定义,使得类型更加动态和灵活。
interface TreeNode<T> {
value: T;
left?: TreeNode<T>;
right?: TreeNode<T>;
}
const tree: TreeNode<number> = {
value: 10,
left: { value: 5 },
right: { value: 15 }
};
在这个例子中,TreeNode 是一个递归类型,它包含了 left 和 right 子节点,每个节点的类型为 TreeNode<T>。
总结
TypeScript 的泛型提供了极大的灵活性,可以在类型层面进行抽象,提高代码的可重用性和类型安全性。通过约束泛型类型,可以确保数据的结构和类型安全。在复杂场景下,泛型还可以和联合类型、递归类型等结合使用,进一步增强类型的表达力。
使用泛型时的一些关键要点包括:
- 类型推导:TypeScript 会根据传入的参数自动推导泛型类型。
- 类型约束:使用
extends关键字为泛型添加约束,确保类型符合预期。 - 默认类型:可以为泛型提供默认类型,使得泛型调用更加简洁。
通过这些实践,开发者可以编写更加灵活且类型安全的代码,适应不同的应用场景。