TypeScript 类、泛型的使用实践记录 | 青训营

115 阅读10分钟

TypeScript

TypeScript 是一种由微软开发的开源编程语言,它在 JavaScript 的基础上添加了静态类型系统,使得代码能够更具可维护性、可读性和安全性。在 TS 中,类和泛型是非常强大的特性,能够在项目开发中提供灵活性和安全性。本篇博客将探讨 TS 中泛型的使用方法和场景,并介绍如何使用类型约束来增加代码的灵活性和安全性。

类和泛型基础

1、类的概念

TS 中,是一种面向对象的编程概念,与C++JAVA等面向对象的编程语言相同,TSJS允许开发者将属性和方法组合在一起,形成一个具有一定功能的对象。在 TS 中,您可以像如下一样轻松地创建和使用类:

// 定义一个类 Container,不使用泛型
class Container {
    private value: any; // 类内部私有属性,存储值,此处使用 any 表示可以是任何类型

    // 构造函数,接收初始值并将其赋给私有属性
    constructor(initialValue: any) {
        this.value = initialValue;
    }

    // 方法:获取存储的值
    getValue(): any {
        return this.value;
    }
}

// 创建一个数字类型的 Container 实例
const numberContainer = new Container(42);
console.log(numberContainer.getValue()); // 输出: 42

// 创建一个字符串类型的 Container 实例
const stringContainer = new Container("Hello, TypeScript");
console.log(stringContainer.getValue()); // 输出: Hello, TypeScript

在这段代码中,我们定义了一个名为 Container 的类,没有使用泛型。类内部有一个私有属性 value 用于存储值,此处我们使用 any 类型表示可以是任何类型的值。构造函数接收初始值,并将其赋给私有属性。getValue 方法用于获取存储的值。

随后,我们创建了两个 Container 实例,一个存储数字类型的值 42,另一个存储字符串类型的值 "Hello, TypeScript"。通过调用实例的 getValue 方法,我们可以分别获取并打印出存储的值。

2、泛型的概念

泛型是一种可以在类、函数和接口中使用的类型参数,它可以在使用的时候指定具体的类型。以下是使用泛型重构后类的声明:

// 定义一个通用的类 Container,使用泛型 T 表示类型参数
class Container<T> {
    private value: T; // 类内部私有属性,存储泛型类型的值

    // 构造函数,接收初始值并将其赋给私有属性
    constructor(initialValue: T) {
        this.value = initialValue;
    }

    // 方法:获取存储的泛型值
    getValue(): T {
        return this.value;
    }
}

// 创建一个数字类型的 Container 实例
const numberContainer = new Container<number>(42);
console.log(numberContainer.getValue()); // 输出: 42

// 创建一个字符串类型的 Container 实例
const stringContainer = new Container<string>("Hello, TypeScript");
console.log(stringContainer.getValue()); // 输出: Hello, TypeScript

在上述代码中,我们同样定义了一个名为 Container 的通用类,不同之处在于这次我们使用了泛型类型参数 T 表示其中的数据类型。该类有一个私有属性 value,用于存储传入的泛型类型的值。构造函数接收一个初始值并将其赋给私有属性。

通过 getValue 方法,我们可以获取存储在容器中的泛型值。在代码末尾,我们展示了如何创建一个存储数字和字符串的 Container 实例,并通过 getValue 方法分别获取和输出存储的值。

通过这个示例,我们可以体悟到泛型的核心思想:创建通用的类、函数或接口,使其可以适用于多种不同类型的数据,从而提高代码的重用性类型安全性

泛型的使用场景

使用泛型后的优势在于代码的灵活性、类型安全性和重用性。让我们通过对比来更清楚地理解这些优势。

1. 灵活性

不使用泛型的情况:

class Container {
    private value: any;

    constructor(initialValue: any) {
        this.value = initialValue;
    }

    getValue(): any {
        return this.value;
    }
}

在上述代码中,我们使用了 any 类型来存储值,这意味着我们可以存储任何类型的数据。然而,这种灵活性带来了潜在的问题,因为我们无法在编译时就捕获类型错误。

使用泛型的情况:

class Container<T> {
    private value: T;

    constructor(initialValue: T) {
        this.value = initialValue;
    }

    getValue(): T {
        return this.value;
    }
}

通过使用泛型,我们可以在类中指定存储的具体类型。这样一来,在创建实例时就能确定存储的类型,在保持代码灵活性的基础上提供了更好的编译时类型检查,避免了潜在的运行时类型错误。

2. 类型安全性

不使用泛型的情况:

const numberContainer = new Container(42);
const stringValue = numberContainer.getValue(); // 在编译时没有错误,但是运行时可能出错

在上述代码中,我们将一个数字类型的值存储在 Container 实例中,然后尝试将存储的值赋给一个变量。虽然在编译时没有错误,但实际上 stringValue 变量可能会包含不是字符串的数据,这可能导致运行时错误。

使用泛型的情况:

const numberContainer = new Container<number>(42);
const stringValue = numberContainer.getValue(); // 编译时报错:不能将类型 'number' 分配给类型 'string'

通过使用泛型,我们在编译时就能捕获潜在的类型错误。在上述代码中,由于我们在创建实例时指定了存储的类型为 number,所以在尝试将存储的值赋给 stringValue 变量时会立即报错。

3. 重用性

使用泛型还能大幅增强代码的重用性,让相同的逻辑能够适用于不同的数据类型。

class Queue<T> {
    private items: T[] = [];

    enqueue(item: T): void {
        this.items.push(item);
    }

    dequeue(): T | undefined {
        return this.items.shift();
    }
}

const numberQueue = new Queue<number>();
numberQueue.enqueue(1);
numberQueue.enqueue(2);

const stringQueue = new Queue<string>();
stringQueue.enqueue("Hello");
stringQueue.enqueue("World");

在上述代码中,Queue 类定义了一个通用的队列,可以适用于不同类型的数据。我们创建了一个存储数字的队列和一个存储字符串的队列,它们使用相同的逻辑来管理不同类型的数据。

总之,使用泛型可以带来更大的灵活性、类型安全性和重用性,从而提高代码质量、可维护性和开发效率。

类型约束的实践

类型约束在实际项目中的应用是非常有价值的,它能够在编译阶段捕获潜在的问题,从而避免在运行时出现错误。让我们通过一个例子来更好地理解在何种情况下使用类型约束可以提高代码的安全性和可靠性。

假设我们要编写一个函数,用于计算数组中所有数字的平均值。然而,我们希望函数仅处理数字类型的数组,以防止不正确的数据类型导致计算错误。

// 使用类型约束来计算数字数组的平均值
function calculateAverage<T extends number[]>(numbers: T): number {
    const sum = numbers.reduce((acc, num) => acc + num, 0);
    return sum / numbers.length;
}

// 正确的数字数组
const validNumberArray = [1, 2, 3, 4, 5];
const averageValid = calculateAverage(validNumberArray);
console.log(`平均值: ${averageValid}`); // 输出: 平均值: 3

// 错误的数组,包含非数字元素
const invalidNumberArray = [1, 2, 3, "4", 5];
const averageInvalid = calculateAverage(invalidNumberArray); // 编译时报错:类型“string”的参数不能赋给类型“number”的参数

在这个例子中,我们定义了一个函数 calculateAverage,它接收一个类型参数 T,该参数被约束为一个数字数组类型(number[])。这意味着只有传入数字数组,才能调用这个函数。

函数内部,我们使用数组的 reduce 方法计算数字的总和,然后除以数组长度得到平均值。因为我们对输入参数进行了类型约束,所以我们可以放心地执行这些操作,不必担心输入数据的类型错误。

接下来,我们创建一个数字数组 numberArray,并调用 calculateAverage 函数来计算平均值。由于输入的是数字数组,所以函数会正常工作并返回正确的结果。

通过这个例子,我们可以看到类型约束如何在编译时捕获潜在错误,使我们能够在运行代码之前就避免类型相关的问题。类型约束不仅可以用于基本类型,还可以用于接口、类等复杂数据结构,为我们的代码添加更多的可靠性和安全性。

提升代码质量:编译时报错 vs. 运行时报错

通过上面的介绍相信大家基本上应该能够理解TS泛型的原理及对应的编译时报错,但有些同学可能还是会提出疑问,编译时报错和运行时报错有什么区别吗?该错不还是得错?

编写稳定可靠的代码是每位开发者的目标,但错误在编码过程中是不可避免的。为了提高代码的质量,开发者们采用各种策略,而其中之一就是利用编译时报错的优势。接下来就让我们继续探讨一下编译时报错与运行时报错之间的区别,以及为何通过在编译时捕获错误可以明显改善代码质量。

编译时报错

编译时报错是在代码被编译成可执行程序之前,由编译器检测到的错误。编译器会分析代码的结构、类型和语法,以确保代码符合语言规范。如果存在拼写错误、类型不匹配或其他语法问题,编译器会发出错误警告,阻止代码编译成可执行程序。

编译时报错的优势在于,它们在代码运行之前就能够捕获问题,从而减少在运行时可能遇到的错误。通过修复编译时错误,您可以确保代码的基本结构和逻辑是正确的

运行时报错

运行时报错发生在程序已经编译成功并开始运行时。这些错误通常由于程序在运行时遇到无法处理的情况,例如除以零、空指针引用、类型转换错误等。运行时报错可能导致程序异常终止或产生意外行为,进而导致程序崩溃。

尽管在运行时报错后,您可以检查错误信息以确定问题所在,但此时已无法避免错误的发生,程序流程已受到干扰

改进代码质量

编译时报错和运行时报错的区别在于发生时间和影响范围。编译时报错在代码编写阶段就能够发现问题,而运行时报错则发生在程序运行时

改进代码质量的过程确实包括修复错误,但通过在编译时解决问题,您可以避免在运行时遇到明显的错误,提高代码的质量、可维护性和可靠性。编译器的错误信息通常会提供有关问题的详细信息,有助于更快地定位和修复问题

在构建稳健的应用程序时,合理地利用编译时报错,可以大大提高代码的质量,从而为用户提供更好的体验和可靠性

总结

通过本文的学习,相信您已经初步了解了 TS泛型的基本概念,以及它们在实际项目中的应用场景。泛型不仅能够增加代码的复用性,还能通过类型约束提升代码的灵活性安全性。在实际开发中,合理运用泛型和类型约束将为您的项目带来更多的好处。随着前端工程化的进展,TS逐渐成为了开发必备的知识,为了更深层次地理解TS的数据类型及应用,[推荐](typescript史上最强学习入门文章(2w字) - 掘金 (juejin.cn))这篇文章,有兴趣的同学建议可以反复阅读,多多实践。