TypeScript 类、泛型的使用实践记录 | 豆包MarsCode AI刷题

44 阅读4分钟

一、泛型基础概念与使用方法

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),明确告诉编译器Tnumber类型。

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 泛型配合类型约束从函数、类到复杂逻辑处理,提供了编写灵活且安全代码的有力工具,让代码复用、类型管控达到更优平衡,适应多样编程场景需求。