在文章 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 的,那么你也能够明白为什么“长相相同名称不同”的两个枚举类型是不兼容得了。
如何转换两个具有相同值的枚举类型?
那么,该如何在 EnumA 和 EnumB 之间进行转换呢?下面介绍几种常见方法:
方法一:类型断言(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