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

50 阅读6分钟

TypeScript 类与泛型使用实践全解析

一、引言

TypeScript 作为 JavaScript 的超集,为前端开发带来了强大的类型系统。其中,类和泛型是两个非常重要的特性。类允许我们以面向对象的方式组织代码,而泛型则提供了一种创建可复用组件的强大方式,可以在不同类型的数据上进行操作,同时通过类型约束确保代码的安全性和灵活性。本文将深入探讨 TypeScript 中泛型的使用方法、应用场景,以及如何利用类型约束来优化代码。

二、TypeScript 类基础回顾

(一)类的定义与实例化

在 TypeScript 中,类使用 class 关键字定义。例如:

收起

typescript

复制

class Person {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  sayHello() {
    console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
  }
}

const person = new Person('John', 30);
person.sayHello(); 

这里定义了一个 Person 类,包含 name 和 age 两个属性,以及一个 sayHello 方法。通过构造函数初始化属性,并可以创建类的实例并调用实例方法。

(二)类的继承

类的继承是面向对象编程中的重要概念,它允许我们创建一个新类,从现有类继承属性和方法。在 TypeScript 中,使用 extends 关键字实现继承。例如:

收起

typescript

复制

class Employee extends Person {
  jobTitle: string;

  constructor(name: string, age: number, jobTitle: string) {
    super(name, age);
    this.jobTitle = jobTitle;
  }

  introduceJob() {
    console.log(`I work as a ${this.jobTitle}.`);
  }
}

const employee = new Employee('Alice', 25, 'Developer');
employee.sayHello();
employee.introduceJob();

Employee 类继承自 Person 类,继承了 nameage 属性和 sayHello 方法,并新增了 jobTitle 属性和 introduceJob 方法。在构造函数中,通过 super 关键字调用父类的构造函数进行初始化。

三、泛型的基本概念与使用方法

(一)泛型函数

泛型函数是可以适用于多种类型的函数。例如,我们可以创建一个函数来获取数组中的第一个元素:

收起

typescript

复制

function getFirstElement<T>(arr: T[]): T {
  return arr[0];
}

const numbers: number[] = [1, 2, 3];
const firstNumber = getFirstElement(numbers);
console.log(firstNumber); 

const strings: string[] = ['a', 'b', 'c'];
const firstString = getFirstElement(strings);
console.log(firstString); 

这里的 <T> 是类型参数,T 可以代表任何类型。函数 getFirstElement 接受一个类型为 T 的数组,并返回数组中的第一个元素,其类型也为 T。这样,这个函数就可以用于不同类型的数组,而不需要为每种类型都编写一个特定的函数。

(二)泛型类

泛型类与泛型函数类似,允许类在实例化时指定类型参数。例如,我们创建一个简单的栈类:

收起

typescript

复制

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

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

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

const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
const poppedNumber = numberStack.pop();
console.log(poppedNumber); 

const stringStack = new Stack<string>();
stringStack.push('a');
stringStack.push('b');
const poppedString = stringStack.pop();
console.log(poppedString); 

Stack 类使用类型参数 Titems 数组存储类型为 T 的元素。push 方法接受类型为 T 的元素并添加到栈中,pop 方法返回栈顶元素,其类型为 T 或 undefined(当栈为空时)。通过在实例化时指定不同的类型参数,可以创建不同类型的栈。

四、泛型的应用场景

(一)数据结构操作

在处理各种数据结构如数组、链表、树等时,泛型非常有用。以链表为例:

收起

typescript

复制

class Node<T> {
  value: T;
  next: Node<T> | null;

  constructor(value: T, next: Node<T> | null = null) {
    this.value = value;
    this.next = next;
  }
}

class LinkedList<T> {
  private head: Node<T> | null = null;

  add(item: T): void {
    const newNode = new Node<T>(item);
    if (!this.head) {
      this.head = newNode;
    } else {
      let current = this.head;
      while (current.next) {
        current = current.next;
      }
      current.next = newNode;
    }
  }

  print(): void {
    let current = this.head;
    while (current) {
      console.log(current.value);
      current = current.next;
    }
  }
}

const numberList = new LinkedList<number>();
numberList.add(1);
numberList.add(2);
numberList.add(3);
numberList.print(); 

const stringList = new LinkedList<string>();
stringList.add('a');
stringList.add('b');
stringList.add('c');
stringList.print(); 

这里的 Node 类和 LinkedList 类都使用了泛型,使得它们可以处理不同类型的数据,无论是数字还是字符串,都能方便地构建和操作链表结构。

(二)数据处理与转换

在进行数据处理和转换操作时,泛型也能发挥重要作用。例如,编写一个函数将一个数组中的元素转换为另一种类型:

收起

typescript

复制

function mapArray<T, U>(arr: T[], callback: (item: T) => U): U[] {
  return arr.map(callback);
}

const numbers = [1, 2, 3];
const squaredNumbers = mapArray(numbers, (num) => num ** 2);
console.log(squaredNumbers); 

const strings = ['1', '2', '3'];
const parsedNumbers = mapArray(strings, (str) => parseInt(str));
console.log(parsedNumbers); 

mapArray 函数接受一个类型为 T 的数组和一个将 T 转换为 U 的回调函数,返回一个类型为 U 的数组。通过泛型,这个函数可以灵活地处理不同类型的数组转换操作。

五、类型约束在泛型中的应用

(一)基本类型约束

有时候,我们希望泛型函数或类只接受特定类型或满足特定条件的类型。例如,创建一个函数来比较两个值的大小,要求这两个值必须是可比较的类型(如数字或字符串):

收起

typescript

复制

function compare<T extends string | number>(a: T, b: T): number {
  if (a === b) {
    return 0;
  } else if (a < b) {
    return -1;
  } else {
    return 1;
  }
}

const result1 = compare(1, 2);
console.log(result1); 

const result2 = compare('a', 'b');
console.log(result2); 

这里的 <T extends string | number> 就是类型约束,限制了 T 只能是 string 或 number 类型,这样在函数内部进行比较操作时就不会出现类型错误。

(二)接口约束

除了基本类型约束,还可以使用接口来约束泛型。例如,定义一个接口 Shape,并创建一个函数来计算不同形状的面积:

收起

typescript

复制

interface Shape {
  area(): number;
}

class Circle implements Shape {
  radius: number;

  constructor(radius: number) {
    this.radius = radius;
  }

  area(): number {
    return Math.PI * this.radius ** 2;
  }
}

class Rectangle implements Shape {
  width: number;
  height: number;

  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }

  area(): number {
    return this.width * this.height;
  }
}

function calculateArea<T extends Shape>(shape: T): number {
  return shape.area();
}

const circle = new Circle(5);
const circleArea = calculateArea(circle);
console.log(circleArea); 

const rectangle = new Rectangle(3, 4);
const rectangleArea = calculateArea(rectangle);
console.log(rectangleArea); 

calculateArea 函数接受一个满足 Shape 接口的泛型类型 T,这样就确保了传入的参数必须有一个 area 方法来计算面积,提高了代码的安全性和可维护性。

六、总结

TypeScript 中的类和泛型为我们提供了强大的工具来构建可复用、安全且灵活的代码。类使我们能够以面向对象的方式组织代码,而泛型则在处理不同类型数据时大放异彩。通过合理应用泛型的各种使用方法和场景,以及利用类型约束来限制泛型的类型范围,我们可以编写更加健壮、通用的代码。无论是在数据结构的实现、数据处理还是在遵循特定接口规范的代码编写中,类与泛型的结合都能显著提升代码的质量和开发效率,为大型项目的开发和维护奠定坚实的基础。在实际开发中,深入理解和熟练运用这些特性,将有助于我们更好地应对各种复杂的编程需求,编写出高质量的 TypeScript 代码。