如何在 TypeScript 中防止枚举值冲突

huangapple go评论59阅读模式
英文:

How to prevent enum values collision in Typescript

问题

我在我的应用程序中有大量的枚举,其中一些枚举具有相同的值(尽管来自不同的枚举类型)。我还有一个通用函数,它接受任何这些枚举并尝试基于枚举类型和值来描述/处理。问题在于如果键相同,枚举值会重复。

有没有办法解决这个问题?我在下面说明了问题:

enum Car {
  bmw = "bmw",
  ferrari = "ferrari"
}

enum Brand {
  bmw = "bmw",
  ferrari = "ferrari"
}

function describeIt(it: string) {
  switch (it) {
    case Car.bmw:
      console.log("It's a BMW car");
      break
    case Brand.bmw:
      console.log("It's the BMW brand");
      break
    case Car.ferrari:
      console.log("It's a Ferrari car");
      break
    case Brand.ferrari:
      console.log("It's the Ferrari brand");
      break
  }
}

describeIt(Car.bmw)   // 得到: It's a BMW car, 正确!
describeIt(Brand.bmw) // 得到: It's a BMW car, 期望: It's the BMW brand

Typescript Playground上试一试。


我有90%的把握,如果我问“如何生成跨所有枚举值的唯一值?”会成为一个XY问题,所以我问“如何防止枚举值冲突?”这才是核心问题。


如果可能的话,我希望保留简单的语法,我知道我可以像这样在枚举类型之前添加前缀:

enum Brand {
  bmw = "Brand.bmw",
  ferrari = "Brand.ferrari"
}

但这不是我要找的解决方案,我希望解决方案来自枚举类型本身。


我已经尝试搜索了很多问题,但没有一个问题解决了这个特定的问题。

英文:

I have a plenty of enums across my application, and some of these enums have the same values (although from different enum types). I also have a generic function that takes any of these enums and try to describe/handle based on the enum type and value. The problem is that the enum values are duplicated if the key is the same.

Is there some way to fix this issue? I've illustrated the problem below:

enum Car {
  bmw = "bmw",
  ferrari = "ferrari"
}

enum Brand {
  bmw = "bmw",
  ferrari = "ferrari"
}

function describeIt(it: string) {
  switch (it) {
    case Car.bmw:
      console.log("It's a BMW car");
      break
    case Brand.bmw:
      console.log("It's the BMW brand");
      break
    case Car.ferrari:
      console.log("It's a Ferrari car");
      break
    case Brand.ferrari:
      console.log("It's the Ferrari brand");
      break
  }
}

describeIt(Car.bmw)   // Got: It's a BMW car, correct!
describeIt(Brand.bmw) // Got: It's a BMW car, expected: It's the BMW brand

Try it at the Typescript Playground.


I'm 90% sure this would be a X Y problem if I asked "How to generate a unique value across all enum values?" so instead I'm asking "How to prevent enum values collision?" that is the core problem here.


I would like to keep the simple syntax if possible, I know that I can prepend the enum type like this, for instance:

enum Brand {
  bmw = "Brand.bmw",
  ferrari = "Brand.ferrari"
}

But this is not what I'm looking for, I want a solution that comes from the enum type itself.


I've tried searching across many questions but none of them addresses this specific problem.

答案1

得分: 1

switch 语句根据 严格相等性 比较算法(参考:MDNECMAScript 规范)将表达式与 case 值进行匹配。

因此,相同字符串的字符串枚举值将始终被视为相等(即使在不同的枚举中),并且具有相同字符串值的第一个 case 将始终匹配。

为了区分值并仍然使用 "enum" 习惯用法,您可以使用一个包含永远不会相等的值的对象(例如对象、符号等) — TypeScript 手册实际上建议使用此对象模式

对象 vs 枚举

在现代 TypeScript 中,当对象与 as const 配合使用时,您可能不需要枚举:

...

相对于 TypeScript 的 enum,这种格式的最大优点在于它使您的代码库与 JavaScript 的状态保持一致,而且 如果 枚举被添加到 JavaScript 中,您可以切换到额外的语法。

这是一个使用符号值的示例:

TS Playground

type Values<T> = T[keyof T];

const Car = {
  Bmw: Symbol("bmw"),
  Ferrari: Symbol("ferrari"),
} as const;

type Car = Values<typeof Car>;

const Brand = {
  Bmw: Symbol("bmw"),
  Ferrari: Symbol("ferrari"),
} as const;

type Brand = Values<typeof Brand>;

function describeIt(it: Car | Brand): void {
  switch (it) {
    case Car.Bmw:
      console.log("这是宝马汽车");
      break;
    case Brand.Bmw:
      console.log("这是宝马品牌");
      break;
    case Car.Ferrari:
      console.log("这是法拉利汽车");
      break;
    case Brand.Ferrari:
      console.log("这是法拉利品牌");
      break;
  }
}

describeIt(Car.Bmw); // "这是宝马汽车"
describeIt(Brand.Bmw); // "这是宝马品牌"

console.log(Car.Bmw === Car.Bmw); // true
console.log(Car.Bmw === Brand.Bmw); // false

如果出于某种原因,您需要可以序列化为字符串的枚举值,您可以创建一个具有自定义 toStringtoJSON 方法的对象类型,如下所示:

[TS Playground](https://www.typescriptlang.org/play?noUncheckedIndexedAccess=true&amp;removeComments=true&amp;target=99&amp;jsx=4&amp;exactOptionalPropertyTypes=true&amp;inlineSourceMap=true&amp;inlineSources=true&amp;isolatedModules=true&amp;noImplicitOverride=true&amp;ssl=44&amp;ssc=45&amp;pln=47&amp;pc=49#code/MYGwhgzhAECqB2BLAjgVwKYGUAuAnR8A5tAN4BQ00wA9vBHqsNtbgBQAOqARiIsNADcwIDAC5o9fEQCUpAL4VozHFMKtp4yQWIloudNlS54SgBaIIAOiEj0AbmgLKzAFKYA8gDl1mvNtJ6BkYm2OZWNhgOCgpk2ACe7OjQAGrCGBAAPAAqAHzQALzQWQDaANbocdQAZkUAunZkZDR02NAAwmC4BaSKAEIAtgDu

英文:

switch statements match expressions against case values according to the strict equality comparison algorithm (ref: MDN, ECMAScript spec).

Because of this, string enum values that are the same string will always compare as equal (even if in different enums) — and the first case with the same string value will always match.

In order to discriminate values and still use the "enum" idiom, you can use an object with values which will never compare as equal (e.g. objects, symbols, etc.) — the TypeScript handbook actually suggests this object pattern:

> ## Objects vs Enums
>
> In modern TypeScript, you may not need an enum when an object with as const could suffice:
>
> ...
>
> The biggest argument in favour of this format over TypeScript’s enum is that it keeps your codebase aligned with the state of JavaScript, and when/if enums are added to JavaScript then you can move to the additional syntax.

Here's an example using symbol values:

TS Playground

type Values&lt;T&gt; = T[keyof T];

const Car = {
  Bmw: Symbol(&quot;bmw&quot;),
  Ferrari: Symbol(&quot;ferrari&quot;),
} as const;

type Car = Values&lt;typeof Car&gt;;

const Brand = {
  Bmw: Symbol(&quot;bmw&quot;),
  Ferrari: Symbol(&quot;ferrari&quot;),
} as const;

type Brand = Values&lt;typeof Brand&gt;;

function describeIt(it: Car | Brand): void {
  switch (it) {
    case Car.Bmw:
      console.log(&quot;It&#39;s a BMW car&quot;);
      break;
    case Brand.Bmw:
      console.log(&quot;It&#39;s the BMW brand&quot;);
      break;
    case Car.Ferrari:
      console.log(&quot;It&#39;s a Ferrari car&quot;);
      break;
    case Brand.Ferrari:
      console.log(&quot;It&#39;s the Ferrari brand&quot;);
      break;
  }
}

describeIt(Car.Bmw); // &quot;It&#39;s a BMW car&quot;
describeIt(Brand.Bmw); // &quot;It&#39;s the BMW brand&quot;

console.log(Car.Bmw === Car.Bmw); // true
console.log(Car.Bmw === Brand.Bmw); // false

And if, for some reason, you need enum values which can be serialized as strings, you could create an object type with customized toString and toJSON methods like this:

TS Playground

class UniqueString {
  constructor(public value: string) {}
  toString(): string { return this.value; }
  toJSON(): string { return this.value; }
}

type Values&lt;T&gt; = T[keyof T];

const Car = {
  Bmw: new UniqueString(&quot;bmw&quot;),
  Ferrari: new UniqueString(&quot;ferrari&quot;),
} as const;

type Car = Values&lt;typeof Car&gt;;

const Brand = {
  Bmw: new UniqueString(&quot;bmw&quot;),
  Ferrari: new UniqueString(&quot;ferrari&quot;),
} as const;

type Brand = Values&lt;typeof Brand&gt;;

// ...etc. (same as previous example)

console.log(String(Car.Bmw)); // &quot;bmw&quot;
console.log(JSON.stringify(Car.Bmw)); // &#39;&quot;bmw&quot;&#39;

huangapple
  • 本文由 发表于 2023年5月24日 18:41:50
  • 转载请务必保留本文链接:https://go.coder-hub.com/76322642.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定