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

384 阅读7分钟

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

什么是泛型?

泛型(generics)是一种编程技巧,可以让我们在定义函数、类、接口、类型别名等时,使用一个或多个类型参数(type parameters),来表示那些在使用时才确定的类型。泛型可以让我们编写更通用、更复用、更灵活、更安全的代码,避免重复或不必要的类型转换。

为什么要使用泛型?

为了理解泛型的作用,我们可以先看一个没有使用泛型的例子。假设我们要定义一个函数,用来返回一个数组中的第一个元素。我们可能会这样写:

// 定义一个返回数组第一个元素的函数
function getFirstElement(array: any[]): any {
  return array[0];
}

// 测试函数
let numbers = [1, 2, 3];
let firstNumber = getFirstElement(numbers); // 返回1
let strings = ["a", "b", "c"];
let firstString = getFirstElement(strings); // 返回"a"

这个函数可以接受任何类型的数组作为参数,并返回数组中的第一个元素。但是,这个函数有两个问题:

  • 它使用了any类型,失去了类型检查的功能。例如,如果我们传入一个空数组,或者一个非数组的值,它也不会报错,而是返回undefined或者抛出异常。
  • 它没有保留参数和返回值之间的类型关系。例如,如果我们传入一个数字数组,我们希望返回值也是一个数字,而不是任意类型。

为了解决这些问题,我们可以使用泛型来改写这个函数:

// 使用泛型定义一个返回数组第一个元素的函数
function getFirstElement<T>(array: T[]): T {
  return array[0];
}

// 测试函数
let numbers = [1, 2, 3];
let firstNumber = getFirstElement(numbers); // 返回1,类型为number
let strings = ["a", "b", "c"];
let firstString = getFirstElement(strings); // 返回"a",类型为string

这里,我们在函数名后面添加了一个类型参数<T>,表示这个函数可以接受任意类型T作为参数,并且返回值也是同样的类型T。这样,我们就可以保证参数和返回值之间的类型一致性,并且避免了使用any类型导致的潜在错误。

如何使用泛型?

在TypeScript中,我们可以在以下几种情况下使用泛型:

  • 函数:我们可以在定义函数时,使用一个或多个类型参数来表示函数的参数或返回值的类型。例如:
// 定义一个交换元组中两个元素位置的函数
function swap<T, U>(tuple: [T, U]): [U, T] {
  return [tuple[1], tuple[0]];
}

// 测试函数
let result = swap([1, "a"]); // 返回["a", 1],类型为[string, number]
  • 类:我们可以在定义类时,使用一个或多个类型参数来表示类的属性或方法的类型。例如:
// 定义一个栈类
class Stack<T> {
  // 使用一个数组来存储栈中的元素
  private items: T[];

  // 构造函数
  constructor() {
    this.items = [];
  }

  // 入栈方法
  push(item: T): void {
    this.items.push(item);
  }

  // 出栈方法
  pop(): T {
    return this.items.pop();
  }

  // 获取栈顶元素方法
  peek(): T {
    return this.items[this.items.length - 1];
  }

  // 判断栈是否为空方法
  isEmpty(): boolean {
    return this.items.length === 0;
  }

  // 获取栈的大小方法
  size(): number {
    return this.items.length;
  }
}

// 测试类
let numberStack = new Stack<number>(); // 创建一个数字栈
numberStack.push(1); // 入栈1
numberStack.push(2); // 入栈2
console.log(numberStack.peek()); // 输出2
console.log(numberStack.pop()); // 出栈2
console.log(numberStack.size()); // 输出1

let stringStack = new Stack<string>(); // 创建一个字符串栈
stringStack.push("a"); // 入栈"a"
stringStack.push("b"); // 入栈"b"
console.log(stringStack.peek()); // 输出"b"
console.log(stringStack.pop()); // 出栈"b"
console.log(stringStack.size()); // 输出1
  • 接口:我们可以在定义接口时,使用一个或多个类型参数来表示接口的属性或方法的类型。例如:
// 定义一个泛型接口,表示一个键值对
interface KeyValuePair<T, U> {
  key: T;
  value: U;
}

// 测试接口
let pair1: KeyValuePair<number, string> = { key: 1, value: "a" }; // 创建一个数字和字符串的键值对
let pair2: KeyValuePair<string, boolean> = { key: "b", value: true }; // 创建一个字符串和布尔值的键值对
  • 类型别名:我们可以在定义类型别名时,使用一个或多个类型参数来表示类型别名的类型。例如:
// 定义一个泛型类型别名,表示一个函数类型
type Callback<T> = (data: T) => void;

// 测试类型别名
let callback1: Callback<number> = (data) => console.log(data + 1); // 创建一个数字类型的回调函数
let callback2: Callback<string> = (data) => console.log(data.toUpperCase()); // 创建一个字符串类型的回调函数

如何使用类型约束?

有时候,我们在使用泛型时,可能需要对泛型的类型进行一些限制,以便我们可以使用一些特定的属性或方法。这时候,我们可以使用类型约束(type constraints)来实现。类型约束是一种使用extends关键字来指定泛型的类型必须满足某个条件的语法。例如,我们可以使用内置的Partial类型来创建一个泛型函数,用来更新一个对象的部分属性:

// 定义一个更新对象部分属性的函数
function update<T>(object: T, partial: Partial<T>): T {
  return { ...object, ...partial };
}

// 测试函数
let person = { name: "Alice", age: 20 };
let updatedPerson = update(person, { age: 21 }); // 返回{name: "Alice", age: 21}

这里,我们使用了Partial<T>类型来表示partial参数的类型,它表示一个对象类型,其中每个属性都是可选的。这样,我们就可以保证partial参数只能包含object参数中已有的属性,并且不会添加新的属性。如果我们不使用Partial<T>类型,而是直接使用T类型,那么我们就可能会传入一些不合法的参数,导致错误。例如:

// 不使用Partial<T>类型的函数定义
function update<T>(object: T, partial: T): T {
  return { ...object, ...partial };
}

// 测试函数
let person = { name: "Alice", age: 20 };
let updatedPerson = update(person, { gender: "female" }); // 编译错误:Type '{ gender: string; }' is not assignable to type '{ name: string; age: number; }'.

这里,我们传入了一个包含gender属性的对象作为partial参数,但是这个属性并不存在于person对象中,所以编译器会报错。如果我们使用了Partial<T>类型,那么编译器就会提示我们只能传入包含name或者age属性的对象。

除了使用内置的泛型类型,我们还可以自己定义一些泛型类型,以适应不同的需求。例如,我们可以定义一个泛型类型,表示一个具有长度属性的类型:

// 定义一个具有长度属性的泛型类型
type Lengthwise<T> = T & { length: number };

这里,我们使用了交叉类型(intersection type)的语法,用&符号来表示两个类型的并集。这样,我们就可以保证Lengthwise<T>类型既包含T类型的所有属性和方法,又包含一个length属性。

有了这个泛型类型,我们就可以对泛型参数进行更精确的约束。例如,我们可以定义一个泛型函数,用来返回一个具有长度属性的参数的长度:

// 定义一个返回长度的泛型函数
function getLength<T extends Lengthwise<T>>(arg: T): number {
  return arg.length;
}

// 测试函数
let array = [1, 2, 3];
let string = "abc";
let object = { length: 10 };
let number = 123;

console.log(getLength(array)); // 输出3
console.log(getLength(string)); // 输出3
console.log(getLength(object)); // 输出10
console.log(getLength(number)); // 编译错误:Argument of type 'number' is not assignable to parameter of type 'Lengthwise<number>'.

这里,我们在定义函数时,使用了extends关键字来表示泛型参数T必须是Lengthwise<T>类型的子类型。这样,我们就可以保证传入的参数一定有length属性,并且返回值一定是一个数字。如果我们传入一个没有length属性的参数,例如一个数字,那么编译器就会报错。

泛型的使用实践记录的总结

泛型是TypeScript中一个非常强大而又实用的特性,它可以让我们编写更通用、更复用、更灵活、更安全的代码。通过使用泛型,我们可以在定义函数、类、接口、类型别名等时,使用一个或多个类型参数来表示那些在使用时才确定的类型。通过使用类型约束,我们可以对泛型参数进行一些限制,以便我们可以使用一些特定的属性或方法。

我希望这些课程笔记对您有所启发和帮助,如果您有任何问题或建议,请随时与我交流。谢谢您!😊