【TypeScript】泛型对象类型

在开发 TypeScript 项目时,理解泛型(Generics)是必不可少的技能之一。TypeScript 通过引入泛型,让我们可以编写灵活且可复用的代码。本文将深入探讨 TypeScript 中的泛型对象类型 (Generic Object Types),特别是通过泛型提升代码的类型安全性、减少重复代码以及提高代码的可扩展性。

一、泛型对象类型的简介

我们可以设想一个可以包含任何类型值的 Box 类型,它可以存储字符串、数字、长颈鹿,甚至任何你能想到的值。在 TypeScript 中,我们可以通过定义一个接口 Box 来实现:

interface Box {
    
    
  contents: any;
}

这个定义允许 Box 中的 contents 属性存储任意类型的值,尽管它可以工作,但这可能在某些情况下导致类型安全问题。举个例子,当我们试图操作 contents 属性时,TypeScript 不会为我们提供任何类型检查,潜在的类型错误就可能在运行时发生。

1.1 使用 unknown 提升安全性

相比 any,使用 unknown 是一种更加安全的做法,它要求我们在使用 contents 前必须进行类型检查,确保数据类型正确:

interface Box {
    
    
  contents: unknown;
}

let x: Box = {
    
    
  contents: "hello world",
};

if (typeof x.contents === "string") {
    
    
  console.log(x.contents.toLowerCase());
}

console.log((x.contents as string).toLowerCase());

尽管这种方式更安全,但每次都要手动进行类型检查或者使用类型断言,代码冗长且容易出错。因此,我们可以通过进一步优化来解决这一问题。

1.2 为每种内容类型创建不同的 Box 类型

我们可以通过为每种 contents 类型分别创建一个 Box,以避免类型断言或检查:

interface NumberBox {
    
    
  contents: number;
}

interface StringBox {
    
    
  contents: string;
}

interface BooleanBox {
    
    
  contents: boolean;
}

虽然这样可以避免直接使用 anyunknown,但带来了新的问题:我们不得不为每种类型创建不同的 Box,这不仅繁琐,而且随着项目的复杂度增加,可能会导致维护困难。

1.3 使用泛型定义 Box

为了解决这个问题,我们可以引入泛型来简化代码,并确保类型安全性。通过使用泛型,我们可以创建一个通用的 Box,它可以容纳任意类型的内容,而不需要为每种类型单独创建 Box 类型:

interface Box<Type> {
    
    
  contents: Type;
}

let box: Box<string> = {
    
     contents: "hello" };

在这里,Box<Type> 是一个通用的模板,Type 是一个占位符,稍后可以被具体的类型替换。当 TypeScript 遇到 Box<string> 时,它将 Type 替换为 string,从而得到一个类型为 { contents: string } 的对象。

扫描二维码关注公众号,回复: 17423316 查看本文章

1.4 泛型的可复用性

使用泛型的一个显著优势在于它的可复用性。我们可以随时将 Box 类型中的 Type 替换为其他任何类型,而不需要重新定义新的 Box 类型。例如:

interface Apple {
    
    
  // Apple 的具体属性...
}

type AppleBox = Box<Apple>;

这种方式不仅提升了代码的灵活性,也让我们可以使用统一的函数来操作不同类型的 Box,而不必为每种类型编写独立的函数。举个例子:

function setContents<Type>(box: Box<Type>, newContents: Type): void {
    
    
  box.contents = newContents;
}

这里的 setContents 函数可以接受任何类型的 Box,并安全地设置它的 contents,完全不需要重载或编写多个版本的函数。

1.5 泛型类型别名

除了接口之外,TypeScript 中的类型别名(type alias)也可以使用泛型。与接口不同,类型别名不仅可以用于描述对象类型,还可以用于其他类型的组合。例如:

type OrNull<Type> = Type | null;
type OneOrMany<Type> = Type | Type[];

通过这些泛型类型别名,我们可以轻松地定义复杂的数据类型组合,进一步提高代码的灵活性和可维护性。

二、数组类型中的泛型

实际上,TypeScript 的数组类型本身就是泛型类型的一个实例。例如,string[] 就是 Array<string> 的简写形式。当我们定义数组时,数组的元素类型是可以通过泛型来指定的:

function doSomething(value: Array<string>) {
    
    
  // ...
}

let myArray: string[] = ["hello", "world"];
doSomething(myArray);

同理,TypeScript 还提供了其他常见的泛型数据结构,例如 Map<K, V>Set<T>Promise<T>,它们都可以通过泛型支持不同的数据类型。

只读数组类型 ReadonlyArray

在某些情况下,我们希望数组的内容是不可修改的,TypeScript 提供了 ReadonlyArray 类型来实现这一需求:

function doStuff(values: ReadonlyArray<string>) {
    
    
  const copy = values.slice(); // 可以读取数组
  console.log(`The first value is ${
      
      values[0]}`);

  values.push("hello!"); // 错误:不能修改 ReadonlyArray
}

ReadonlyArray 确保了数组的不可变性,而这在某些场景中非常有用,例如函数不应该修改传入的数组参数。需要注意的是,ReadonlyArray 只是一种类型,而不是一种值,因此我们无法直接通过构造函数创建它。

三、元组类型

元组类型是 TypeScript 提供的另一种数组类型,它可以精确地描述数组中每个位置的元素类型。例如:

type StringNumberPair = [string, number];

function doSomething(pair: [string, number]) {
    
    
  const a = pair[0]; // string
  const b = pair[1]; // number
}

元组类型非常适合那些基于位置的 API,其中每个位置的元素类型是已知且固定的。我们可以使用解构赋值操作轻松访问元组的元素,这使得代码更加直观和易读。

可选属性与剩余元素

TypeScript 还允许元组包含可选元素或剩余元素。例如:

type Either2dOr3d = [number, number, number?];

function setCoordinate(coord: Either2dOr3d) {
    
    
  const [x, y, z] = coord;
  console.log(`Provided coordinates had ${
      
      coord.length} dimensions`);
}

通过可选属性,元组可以在不同场景下表示二维或三维的坐标信息。此外,元组还可以包含剩余元素,用于表示不确定数量的元素,例如:

type StringNumberBooleans = [string, number, ...boolean[]];

这种灵活的定义方式使得元组类型在处理参数列表或 API 返回值时非常有用。

四、结论

通过本文的介绍,我们深入探讨了 TypeScript 中的泛型对象类型,以及如何利用泛型提升代码的灵活性、类型安全性和可复用性。无论是通过定义通用的 Box 类型,还是在数组、元组类型中应用泛型,TypeScript 的泛型机制都为开发者提供了强大的工具,帮助我们编写更加健壮、可维护的代码。

推荐:


在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/lph159/article/details/142974911