TypeScript 结构性类型系统的异类:枚举类型

1,919 阅读2分钟

在文章 TypeScript 中的结构性类型系统:为什么类型不同但结构一致就能通过 中我们介绍了 TypeScript 是一种使用 结构性类型系统(Structural Type System) 的编程语言。也就是说,在判断一个类型是否兼容另一个类型时,TypeScript 并不关心它们的“名字”,而是关心它们的“结构”。

什么是结构性类型系统?

在结构性类型系统中,只要一个值的“形状”符合目标类型所要求的结构,就被认为是兼容的。例如:

interface A {
  name: string;
}

function greet(person: A) {
  console.log("Hello, " + person.name);
}

const obj = { name: "Alice", age: 30 };
greet(obj); // ✅ 没问题,obj 拥有符合 A 的结构

即使 obj 不是 A 类型,但只要它包含 A 所需的字段,TypeScript 就认为它是兼容的。这种灵活性是 TypeScript 的一大优势。

枚举的特例:名义类型行为

然而,这种“只看长相不看名号”的规则并不适用于 枚举类型(enum) 。哪怕两个枚举的成员完全一致,TypeScript 也会将它们视为不兼容的不同类型:

enum EnumA {
  Red = 'Red',
  Green = 'Green',
  Blue = 'Blue'
}

enum EnumB {
  Red = 'Red',
  Green = 'Green',
  Blue = 'Blue'
}

function paint(color: EnumA) {}

const myColor: ColorB = EnumB.Green;

// ❌ 报错:Argument of type 'EnumB' is not assignable to parameter of type 'EnumA'
paint(myColor);

为什么会这样?因为 枚举在 TypeScript 中是一种带有“名义类型”特征的结构。即使它们结构相同,TypeScript 仍将它们视为不同的类型,要求显式地进行转换才能互通。这种设计可以防止因错误地混用语义不同的枚举而引发的逻辑错误。

读者老爷们也可以看一看这篇文章,了解一下 TypeScript 是如何将 enum 转化为 JavaScript 的,那么你也能够明白为什么“长相相同名称不同”的两个枚举类型是不兼容得了。

如何转换两个具有相同值的枚举类型?

那么,该如何在 EnumAEnumB 之间进行转换呢?下面介绍几种常见方法:

方法一:类型断言(Type Assertion)

这是最简单直接的方式:

const a: EnumA = EnumA.Red;
const b: EnumB = a as EnumB;

只要两个枚举值完全一致,这种方式是可行的,但缺乏类型安全。


方法二:显式转换函数(推荐)

为了提高代码的可读性和安全性,推荐使用一个封装好的转换函数:

function convertEnumAtoEnumB(a: EnumA): EnumB {
  switch (a) {
    case EnumA.Red:
      return EnumB.Red;
    case EnumA.Green:
      return EnumB.Green;
    case EnumA.Blue:
      return EnumB.Blue;
  }
}

这种方式便于维护,如果将来两个枚举的值发生变化,也可以快速定位问题。


方法三:基于值的动态映射

如果枚举值是字符串或数字且完全一致,也可以通过值来映射:

const a: EnumA = EnumA.Green;
const b: EnumB = EnumB[a as keyof typeof EnumB];

或者使用一个通用的转换函数:

function convertEnumValue<T extends string, U>(value: T, targetEnum: Record<T, U>): U {
  return targetEnum[value];
}

const b = convertEnumValue(a, EnumB); // b 是 EnumB.Green