一、泛型基础概念与使用方法
1. 定义泛型函数
在 TypeScript 中,泛型允许我们编写可复用且能适应多种类型的代码,而不是为每种特定类型都编写重复逻辑。例如,定义一个简单的函数,用于返回传入值本身:
function identity<T>(arg: T): T {
return arg;
}
这里<T>就是定义了一个泛型类型参数,T可以代表任意类型,调用这个函数时,TypeScript 会根据传入参数的实际类型来推断T的具体类型。比如:
let numResult = identity(5); // 此时 T 被推断为 number 类型
let strResult = identity('hello'); // T 被推断为 string 类型
你也可以显式指定类型参数,像这样:identity<number>(5),明确告诉编译器T是number类型。
2. 泛型类
泛型类使得类的成员属性、方法可以操作多种类型数据。考虑一个简单的Box类,用来存储任意类型的值:
class Box<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
}
let numberBox = new Box(10); // T 为 number,numberBox.getValue() 返回 number 类型值
let stringBox = new Box('TypeScript'); // T 为 string,stringBox.getValue() 返回 string 类型值
在类定义处的<T>表明这个类是泛型类,实例化时传入的类型决定了类内部使用的T的具体类型,从而保证类型安全。
3. 泛型接口
泛型接口用于规范一组具有相同结构但操作不同类型的对象行为。示例如下:
interface Pair<T, U> {
first: T;
second: U;
}
let numberPair: Pair<number, string> = { first: 1, second: 'one' };
这个Pair接口可适配不同类型组合,只要遵循first对应T类型、second对应U类型的结构即可,增强了代码描述复杂数据结构的灵活性。
二、泛型使用场景
1. 数据结构操作
在处理数组、链表、树等数据结构时,泛型优势显著。以自定义简单数组类为例,实现添加和获取元素功能:
class MyArray<T> {
private elements: T[] = [];
add(element: T): void {
this.elements.push(element);
}
get(index: number): T {
return this.elements[index];
}
}
let stringArray = new MyArray<string>();
stringArray.add('a');
stringArray.add('b');
console.log(stringArray.get(1)); // 输出 'b',确保类型安全,不会出现类型混淆
无论数组存储数字、字符串还是复杂对象,都能复用这个MyArray类逻辑,通过泛型适配不同数据类型需求。
2. 函数式编程
在编写高阶函数(接受函数作为参数或返回函数的函数)时,泛型很常用。比如,一个通用的映射函数,将数组中每个元素通过给定函数转换:
function mapArray<T, U>(arr: T[], func: (item: T) => U): U[] {
return arr.map(func);
}
let numbers = [1, 2, 3];
let squaredNumbers = mapArray(numbers, (num) => num * num); // 输入 number 数组,输出 number 数组,类型安全转换
这里泛型<T, U>让mapArray适用于不同类型数组及转换规则,输入number数组按规则返回number数组,输入string数组执行字符串操作也能正确类型推断与处理。
三、类型约束增强灵活性与安全性
1. 基本类型约束
通过extends关键字可以约束泛型类型参数必须符合某种类型结构。例如,创建一个函数只处理具有length属性的类型(像字符串、数组等):
function printLength<T extends { length: number }>(arg: T): void {
console.log(arg.length);
}
printLength('hello'); // 正常,字符串有 length 属性
printLength([1, 2, 3]); // 正常,数组有 length 属性
// printLength(5); // 报错,数字没有 length 属性,满足类型安全要求,避免运行时潜在错误
这种约束限定了泛型适用范围,在编译阶段就排查不符合要求的用法,既灵活(支持多类型只要满足约束)又安全。
2. 接口类型约束
结合接口与泛型约束,可精确规范复杂对象操作。假设定义接口Shape,有area方法计算面积,创建一个函数计算不同形状面积和:
interface Shape {
area(): number;
}
function sumAreas<T extends Shape>(shapes: T[]): number {
return shapes.reduce((acc, shape) => acc + shape.area(), 0);
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
area(): number {
return this.width * this.height;
}
}
class Circle implements Shape {
constructor(private radius: number) {}
area(): number {
return Math.PI * this.radius ** 2;
}
}
let rectangles = [new Rectangle(2, 3), new Rectangle(4, 5)];
let circles = [new Circle(2)];
console.log(sumAreas(rectangles));
console.log(sumAreas(circles));
// 若传入不实现 Shape 接口对象会编译报错,保障代码逻辑针对预期形状类型处理,增强安全性与可维护性。
利用类型约束,代码清晰定义输入对象需满足条件,提升整体健壮性,在面对类型扩展、修改场景更易维护,保持灵活性应对多种符合接口规范的形状类加入。
总之,TypeScript 泛型配合类型约束从函数、类到复杂逻辑处理,提供了编写灵活且安全代码的有力工具,让代码复用、类型管控达到更优平衡,适应多样编程场景需求。