MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript 泛型类的继承与多态性探讨

2022-09-065.5k 阅读

TypeScript 泛型类的继承

在 TypeScript 的世界里,泛型类为我们提供了一种创建可复用组件的强大方式。当涉及到泛型类的继承时,有一些关键的概念和规则需要我们深入理解。

泛型类继承基础

假设我们有一个基础的泛型类 BaseGenericClass

class BaseGenericClass<T> {
    value: T;
    constructor(val: T) {
        this.value = val;
    }
    getValue(): T {
        return this.value;
    }
}

现在,我们想要创建一个继承自 BaseGenericClass 的子类 SubGenericClass。在继承时,子类可以选择保持泛型参数不变,也可以指定具体的类型。 保持泛型参数不变的继承方式如下:

class SubGenericClass<T> extends BaseGenericClass<T> {
    additionalValue: string;
    constructor(val: T, addVal: string) {
        super(val);
        this.additionalValue = addVal;
    }
    getAdditionalValue(): string {
        return this.additionalValue;
    }
}

在上述代码中,SubGenericClass 继承了 BaseGenericClass,并保留了相同的泛型参数 T。这样,SubGenericClass 就拥有了 BaseGenericClass 的所有属性和方法,同时还添加了自己的 additionalValue 属性和 getAdditionalValue 方法。

我们可以这样使用这两个类:

let baseInstance = new BaseGenericClass<number>(10);
let subInstance = new SubGenericClass<number>(20, 'extra');
console.log(baseInstance.getValue()); // 输出: 10
console.log(subInstance.getValue()); // 输出: 20
console.log(subInstance.getAdditionalValue()); // 输出: extra

子类指定泛型类型

子类也可以为泛型参数指定具体的类型。例如:

class StringSubGenericClass extends BaseGenericClass<string> {
    prefix: string;
    constructor(val: string, prefix: string) {
        super(val);
        this.prefix = prefix;
    }
    getPrefixedValue(): string {
        return this.prefix + this.value;
    }
}

这里,StringSubGenericClass 继承自 BaseGenericClass,并将泛型参数 T 具体指定为 string。然后它添加了 prefix 属性和 getPrefixedValue 方法。 使用示例如下:

let stringSubInstance = new StringSubGenericClass('world', 'hello ');
console.log(stringSubInstance.getPrefixedValue()); // 输出: hello world

泛型类继承中的类型约束

在泛型类继承过程中,类型约束起着重要的作用。它可以确保泛型参数满足特定的条件,从而增强代码的健壮性和可靠性。

基于接口的类型约束

假设我们有一个接口 HasLength

interface HasLength {
    length: number;
}

我们可以在泛型类继承时,对泛型参数进行约束,使其必须符合 HasLength 接口。修改 BaseGenericClass 如下:

class BaseGenericClass<T extends HasLength> {
    value: T;
    constructor(val: T) {
        this.value = val;
    }
    getLength(): number {
        return this.value.length;
    }
}

现在,BaseGenericClass 的泛型参数 T 必须是具有 length 属性的类型。例如字符串、数组等。

let stringBaseInstance = new BaseGenericClass('test');
let arrayBaseInstance = new BaseGenericClass([1, 2, 3]);
console.log(stringBaseInstance.getLength()); // 输出: 4
console.log(arrayBaseInstance.getLength()); // 输出: 3

如果我们尝试传入不满足 HasLength 接口的类型,如数字,TypeScript 编译器会报错:

// 以下代码会报错
// let numberBaseInstance = new BaseGenericClass(10);
// 错误信息: 类型“number”不满足约束“HasLength”。
// 类型“number”缺少属性“length”

子类继承时的类型约束扩展

当子类继承泛型类时,子类可以进一步扩展类型约束。比如,我们创建一个继承自 BaseGenericClass 的子类 SubLengthGenericClass

interface IsString extends HasLength {
    toUpperCase(): string;
}
class SubLengthGenericClass<T extends IsString> extends BaseGenericClass<T> {
    getUppercaseValue(): string {
        return this.value.toUpperCase();
    }
}

这里,SubLengthGenericClass 的泛型参数 T 不仅要满足 HasLength 接口,还要满足 IsString 接口,即必须具有 toUpperCase 方法。 使用示例:

let subStringInstance = new SubLengthGenericClass('lower');
console.log(subStringInstance.getUppercaseValue()); // 输出: LOWER

泛型类继承中的多态性

多态性是面向对象编程的重要特性之一,在 TypeScript 的泛型类继承中同样存在多态性的体现。

方法重写与多态

在泛型类继承中,子类可以重写父类的方法,从而实现多态行为。继续以之前的 BaseGenericClassSubGenericClass 为例,假设 BaseGenericClass 有一个 printValue 方法:

class BaseGenericClass<T> {
    value: T;
    constructor(val: T) {
        this.value = val;
    }
    getValue(): T {
        return this.value;
    }
    printValue(): void {
        console.log(`Base value: ${this.value}`);
    }
}

子类 SubGenericClass 可以重写 printValue 方法:

class SubGenericClass<T> extends BaseGenericClass<T> {
    additionalValue: string;
    constructor(val: T, addVal: string) {
        super(val);
        this.additionalValue = addVal;
    }
    getAdditionalValue(): string {
        return this.additionalValue;
    }
    printValue(): void {
        console.log(`Sub value: ${this.value}, Additional: ${this.additionalValue}`);
    }
}

现在,当我们创建 BaseGenericClassSubGenericClass 的实例,并调用 printValue 方法时,会看到不同的输出:

let baseInstance = new BaseGenericClass<number>(10);
let subInstance = new SubGenericClass<number>(20, 'extra');
baseInstance.printValue(); 
// 输出: Base value: 10
subInstance.printValue(); 
// 输出: Sub value: 20, Additional: extra

这就是方法重写带来的多态性,相同的方法名在不同的类实例上表现出不同的行为。

泛型类型的多态体现

泛型类的多态性还体现在泛型类型的使用上。例如,我们有一个函数 printGenericClass,它接受一个 BaseGenericClass 类型的参数:

function printGenericClass(instance: BaseGenericClass<any>) {
    instance.printValue();
}

这个函数可以接受 BaseGenericClass 及其子类的实例,因为子类也是 BaseGenericClass 的一种类型。

printGenericClass(baseInstance); 
// 输出: Base value: 10
printGenericClass(subInstance); 
// 输出: Sub value: 20, Additional: extra

这里,虽然 printGenericClass 只接受 BaseGenericClass 类型的参数,但由于多态性,它可以处理 SubGenericClass 的实例,并且会调用相应实例的重写方法,展现出不同的行为。

泛型类继承与多态的高级应用

在实际的前端开发中,泛型类继承与多态性有着许多高级的应用场景。

数据存储与操作类的设计

在前端开发中,我们经常需要处理不同类型的数据存储和操作。例如,我们可以设计一个基础的泛型数据存储类 DataStore

class DataStore<T> {
    data: T[];
    constructor() {
        this.data = [];
    }
    add(item: T): void {
        this.data.push(item);
    }
    get(index: number): T | undefined {
        return this.data[index];
    }
    getAll(): T[] {
        return this.data;
    }
}

然后,我们可以创建一些子类来扩展其功能。比如,一个用于存储用户信息的 UserStore,假设用户信息有一个 id 属性:

interface User {
    id: number;
    name: string;
}
class UserStore extends DataStore<User> {
    getById(id: number): User | undefined {
        return this.data.find(user => user.id === id);
    }
}

这里,UserStore 继承自 DataStore,并将泛型参数指定为 User。同时,它添加了 getById 方法来根据用户 id 获取用户信息。 使用示例:

let userStore = new UserStore();
userStore.add({ id: 1, name: 'Alice' });
userStore.add({ id: 2, name: 'Bob' });
console.log(userStore.getById(1)); 
// 输出: { id: 1, name: 'Alice' }

组件复用与定制

在前端组件开发中,泛型类继承和多态性可以实现组件的高度复用和定制。例如,我们有一个基础的泛型列表组件类 BaseList

class BaseList<T> {
    items: T[];
    constructor(items: T[]) {
        this.items = items;
    }
    renderItem(item: T): string {
        return JSON.stringify(item);
    }
    render(): string {
        let result = '<ul>';
        this.items.forEach(item => {
            result += `<li>${this.renderItem(item)}</li>`;
        });
        result += '</ul>';
        return result;
    }
}

这个 BaseList 类可以渲染任何类型的列表。现在,我们创建一个用于渲染用户列表的子类 UserList

class UserList extends BaseList<User> {
    renderItem(user: User): string {
        return `${user.name} (ID: ${user.id})`;
    }
}

UserList 继承自 BaseList,并将泛型参数指定为 User,同时重写了 renderItem 方法来定制用户列表项的渲染方式。 使用示例:

let users = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' }
];
let userList = new UserList(users);
console.log(userList.render()); 
// 输出: <ul><li>Alice (ID: 1)</li><li>Bob (ID: 2)</li></ul>

通过这种方式,我们可以基于基础的泛型组件类,快速创建出满足特定需求的定制化组件,实现组件的高效复用。

泛型类继承与多态的注意事项

在使用泛型类继承与多态性时,有一些注意事项需要我们关注,以避免潜在的问题。

类型兼容性问题

在泛型类继承中,要注意类型兼容性。例如,虽然子类是父类的一种类型,但泛型类型参数的变化可能会影响类型兼容性。 假设我们有两个泛型类 GenericClassAGenericClassB

classGenericClassA<T> {
    value: T;
    constructor(val: T) {
        this.value = val;
    }
}
classGenericClassB<T extends number> extendsGenericClassA<T> {
    // 这里泛型参数 T 被约束为 number 类型
}

现在,如果我们尝试将 GenericClassB<number> 的实例赋值给 GenericClassA<string> 类型的变量,会出现类型错误:

// 以下代码会报错
// let a:GenericClassA<string> = newGenericClassB(10);
// 错误信息: 类型“GenericClassB<number>”不能赋值给类型“GenericClassA<string>”。
// 类型“number”不能赋值给类型“string”

这是因为 GenericClassB 的泛型参数 T 被约束为 number 类型,与 GenericClassA<string> 的类型不兼容。

重写方法的签名一致性

当子类重写父类的方法时,方法签名必须保持一致(除了返回类型可以是协变的情况)。例如,在 BaseGenericClassSubGenericClass 的例子中,如果 SubGenericClass 重写 printValue 方法时改变了参数列表或返回类型,TypeScript 编译器会报错。

class BaseGenericClass<T> {
    value: T;
    constructor(val: T) {
        this.value = val;
    }
    printValue(): void {
        console.log(`Base value: ${this.value}`);
    }
}
class SubGenericClass<T> extends BaseGenericClass<T> {
    // 以下重写会报错,因为返回类型不一致
    // printValue(): string {
    //     return `Sub value: ${this.value}`;
    // }
    // 以下重写会报错,因为参数列表不一致
    // printValue(newVal: T): void {
    //     console.log(`Sub value: ${newVal}`);
    // }
}

确保重写方法的签名一致性可以保证多态行为的正确性和可预测性。

泛型参数的传递与保持

在泛型类继承中,要注意泛型参数的传递和保持。如果子类没有正确处理泛型参数,可能会导致类型错误或功能异常。例如,在 SubGenericClass 继承 BaseGenericClass 时,如果 SubGenericClass 没有正确传递泛型参数 T 到父类的构造函数中,可能会导致父类的属性和方法无法正确工作。

class BaseGenericClass<T> {
    value: T;
    constructor(val: T) {
        this.value = val;
    }
    getValue(): T {
        return this.value;
    }
}
class SubGenericClass<T> extends BaseGenericClass<T> {
    additionalValue: string;
    // 错误示例,没有正确传递泛型参数到父类构造函数
    // constructor(addVal: string) {
    //     super(); // 这里缺少泛型参数 val
    //     this.additionalValue = addVal;
    // }
    constructor(val: T, addVal: string) {
        super(val);
        this.additionalValue = addVal;
    }
    getAdditionalValue(): string {
        return this.additionalValue;
    }
}

正确传递泛型参数是保证泛型类继承和多态性正常工作的基础。

总结

通过深入探讨 TypeScript 泛型类的继承与多态性,我们了解到它们在前端开发中提供了强大的代码复用和灵活性。从基础的泛型类继承、类型约束,到多态性的实现和高级应用,以及相关的注意事项,每一个环节都为我们构建健壮、可维护的前端代码提供了有力的支持。在实际开发中,合理运用泛型类继承与多态性,可以提高代码的质量和开发效率,让我们能够更好地应对复杂多变的业务需求。无论是设计数据存储类、组件复用,还是处理类型相关的逻辑,掌握这些知识都将使我们在 TypeScript 的编程世界中更加得心应手。