从JSON字符串生成Typescript `enum`

79

有没有办法使 TypeScript 枚举与 JSON 中的字符串兼容?

例如:

enum Type { NEW, OLD }

interface Thing { type: Type }

let thing:Thing = JSON.parse('{"type": "NEW"}');

alert(thing.type == Type.NEW); // false

我希望 thing.type == Type.NEW 成立。更具体地说,我希望能够指定enum值为字符串而不是数字。

我知道我可以使用 thing.type.toString() == Type[Type.NEW],但这很麻烦,似乎会使枚举类型注释变得混乱和误导性,这违背了它的目的。JSON 技术上并没有提供有效的枚举值,所以我不应该将属性类型定义为枚举。

因此,我现在正在使用一个包含静态常量的字符串类型:

const Type = { NEW: "NEW", OLD: "OLD" }

interface Thing { type: string }

let thing:Thing = JSON.parse('{"type": "NEW"}');

alert(thing.type == Type.NEW); // true

这样做可以得到我想要的用法,但类型注释 string 太广泛且容易出错。

作为 JavaScript 的超集,没有字符串枚举有些令人惊讶。我错过了什么吗?还有其他实现方式吗?


更新 TS 1.8

使用字符串字面量类型是另一种选择(感谢 @basaret),但要获得所需的类似枚举的用法(如上所述),需要将值定义两次:一次作为字符串字面量类型,一次作为值(常量或命名空间):

type Type = "NEW" | "OLD";
const Type = {
    NEW: "NEW" as Type,
    OLD: "OLD" as Type
}

interface Thing { type: Type }

let thing:Thing = JSON.parse(`{"type": "NEW"}`);

alert(thing.type === Type.NEW); // true

这个可以工作,但需要很多样板代码,足以让我大部分时间不使用它。目前我希望“字符串枚举”的提案最终能够进入路线图。


更新 TS 2.1

新的keyof类型查找 允许从常量或命名空间的键生成字符串字面类型,这使得定义稍微少了一些冗余:

namespace Type {
    export const OLD = "OLD";
    export const NEW = "NEW";
}
type Type = keyof typeof Type;

interface Thing { type: Type }

const thing: Thing = JSON.parse('{"type": "NEW"}');
thing.type == Type.NEW // true

更新TS 2.4

TypeScript 2.4 增加了对字符串枚举的支持!上述示例变为:

enum Type {
    OLD = "OLD",
    NEW = "NEW"
}

interface Thing { type: Type }
const thing: Thing = JSON.parse('{"type": "NEW"}');
alert(thing.type == Type.NEW) // true

这看起来已经非常完美了,但还存在一些心痛:

  • 你仍然需要写两次值,比如OLD = "OLD",而且没有验证你是否有拼写错误,比如NEW = "MEW"...我在真实代码中已经因此遭受过损失。
  • 枚举类型检查存在一些奇怪的问题(可能是bug),它不仅仅是一个字符串文字类型的简写,这才是真正正确的方式。我遇到过一些问题:

    enum Color { RED = "RED", BLUE = "BLUE", GREEN = "GREEN" }
    
    type ColorMap = { [P in Color]: number; }
    
    declare const color: Color;
    declare const map: ColorMap;
    map[color] // Error: Element implicitly has an 'any' type because type 'ColorMap' has no index signature.
    
    const red: Color = "RED"; // Type '"RED"' is not assignable to type 'Color'.
    const blue: Color = "BLUE" as "RED" | "BLUE" | "GREEN"; // Error: Type '"RED" | "BLUE" | "GREEN"' is not assignable to type 'Color'.
    

    使用字符串文字类型代替enum Color的等效代码可以正常工作...

是的,我认为我对此有强迫症,我只想要完美的JS枚举。:)

5个回答

33

如果你在Typescript 2.4版本之前使用枚举,有一种方法可以通过将枚举的值转换为any来实现。

这是你第一个实现的enum Type { NEW = <any>"NEW", OLD = <any>"OLD", } interface Thing { type: Type } let thing:Thing = JSON.parse('{"type": "NEW"}'); alert(thing.type == Type.NEW); // true

Typescript 2.4已经内置了对字符串枚举的支持,因此不再需要将其强制转换为any,而且无需使用字符串字面量联合类型即可实现。这种方式可以用于验证和自动完成,但在可读性和重构方面可能不太好,具体取决于使用场景。


谢谢!我希望在之前就知道“any”断言。现在,我正在尝试TS 2.4字符串枚举,它非常接近我最初想要的...但我发现了一些与TS类型检查方式有关的问题... - Aaron Beall
@Aaron 很酷,很高兴能帮忙!此外,你可能想要检查一下 ts-enums 项目,因为它使枚举处理在许多用例中非常灵活和强大。 - Felipe Sabino
3
哇,这真的是救星!正如我在另一个答案中评论所说的,只有当键名与字符串值匹配时,此方法才能生效,因为对于字符串枚举并不存在反向映射。不过,将字符串值转换为any即可让编译器生成反向映射。如果您可以将这些信息编辑到您的答案中,那将非常有帮助。 - altocumulus
当使用<any>"string value"(consistent-type-assertions)时,您的linter可能会显示警告。"解决方法"就是简单地改为"string value" as any - Kai
好的解决方案。然而,我建议在你的示例中使用与概念无关的变量名。你的 type: Type 会让新手感到困惑。同样的问题也出现在命名为 Typeenum 中。使用明显不相关的名称,比如 "foo" 和 "bar" 或者至少是 "MyEnum"。 - user358041

5

如果有人在2021年仍在关注这个问题:

@Aaron wrote in the original question:

This looks nearly perfect, but there's still some heartache:

You still have to [...]

enum Color { RED = "RED", BLUE = "BLUE", GREEN = "GREEN" }

type ColorMap = { [P in Color]: number; }

declare const color: Color;
declare const map: ColorMap;
map[color] // Error: Element implicitly has an 'any' type because type 'ColorMap' has no index signature.
// [...]

The equivalent code with enum Color replaced by string literal types work fine...

Yeah, I think I have OCD about this, I just want my perfect JS enums. :)

1. keyof typeof enumObj

关于此处,可以使用字符串字面量类型替换枚举类型 Color 的等效代码正常工作...

在链接的连词中使用 typeofkeyof 运算符。

type ColorKeys = keyof typeof Color
type ColorMap = { [P in ColorKeys]: number; } // will have strongly typed keys

访问map: ColorMap时不再隐式使用any。这也适用于数字枚举(应该更多地使用const)。

来自{{link1:Typescript Handbook-编译时的枚举}}:

尽管枚举是实际存在于运行时的对象,但keyof关键字的工作方式与您对典型对象的预期不同。相反,请使用keyof typeof来获取表示所有枚举键为字符串的类型。

2. {{link2:ts-enum-util}}

请查看{{link2:ts-enum-util}},它提供了强类型接口以满足您的所有枚举相关需求(可能)。


3

TS 2.9.2
我的解决方案:

export enum Enums { VALUE1, VALUE2 }

当我从API的json中获取值时:

 switch (response.enumValue.toString()) { //can be without toString if we have string value from JSON.
    case Enums[Enums.VALUE1]:
      ...
    case Enums[Enums.VALUE2]:
      ...
 }

我在我的原始帖子中提到了这一点,这不是我想要的。基本上,这意味着您的JSON不满足有效的枚举值,并且您需要某种互操作代码(我猜您的示例正在显示)。我希望 response.enumValue == Enums.VALUE1 为真并且类型兼容。 - Aaron Beall

1

这看起来很有前途。我们将在升级到TS 1.8时进行调查。我正在尝试使用字符串字面值作为常量,例如thing.type == Type.NEW,但无法解决。 - Aaron Beall
1
这是我能做到的最接近的内容:戳这里,但它需要定义 Type 和值两次,一次作为接口类型注释的 type,一次作为需要值的 const。是否有改进空间? - Aaron Beall

1

我一直在使用转换函数作为权宜之计。希望这个线程能够得到解决: https://github.com/Microsoft/TypeScript/issues/1206

enum ErrorCode {
    Foo,
    Bar
}

interface Error {
    code: ErrorCode;
    message?: string;
}

function convertToError(obj: any): Error {
    let typed: Error = obj as Error;

    // Fix any enums
    typed.code = ErrorCode[typed.code.toString()];
    return typed;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接