如何制作使用字符串文字的TypeScript字符串枚举并正确进行类型推断

时间:2017-06-21 18:50:29

标签: typescript typescript2.0

我想要一个类型来描述一组字符串,以及一个带有键的对象,以便于访问所述字符串。

选项1(不提供正确的类型推断)

初始化const TwoWords时,所有键的值都被类型声明为TwoWords,然后DoSomething(TwoWords.Foo)编译,但switch语句中的typeguard不能按预期工作 - {的类型默认情况下,{1}}不是word

never

选项2(正确的类型推断,但过于冗长)

但是,如果我为每个type TwoWords = 'foo' | 'bar'; const TwoWords = { Foo: 'foo' as TwoWords, Bar: 'bar' as TwoWords }; function DoSomething(word: TwoWords) { switch (word) { case TwoWords.Foo: break; case TwoWords.Bar: break; default: let typeInferenceCheck: never = word; // Type '"foo"' is not assignable to type 'never' } } DoSomething(TwoWords.Foo); DoSomething('bar'); 键的值使用字符串文字类型断言,则默认情况下TwoWords的类型为word,正如我所料。

never

如果type TwoWords = 'foo' | 'bar'; const TwoWords = { Foo: 'foo' as 'foo', Bar: 'bar' as 'bar' }; function DoSomething(word: TwoWords) { switch (word) { case TwoWords.Foo: break; case TwoWords.Bar: break; default: let typeInferenceCheck: never = word; // OK } } DoSomething(TwoWords.Foo); DoSomething('bar'); 'foo'是更长的字符串(例如,整个句子),我不想复制它们 - 它太冗长了。是否有另一种方法可以使用字符串键控枚举,从switch语句(或if / else链)中的类型推断透视图中按预期运行?

选项3(不能与字符串文字互换)

根据Madara Uchiha的回答,您可以使用TypeScript 2.4字符串枚举获得正确的类型推断(如选项2中所示),但这些不可与字符串文字互换。

'bar'

(见GitHub issue #15930 about string literal assignment to TypeScript 2.4 string enum

标准

我正在寻找另一个允许我拥有的选项:

  1. 用于访问字符串文字的枚举式对象 DoSomething('bar'); // Type '"bar"' is not assignable to parameter of type 'TwoWords'
  2. 类型表示只允许枚举成员,是否允许:
    1. 字符串文字 - EnumLikeObject.Foo === 'foo'
    2. 枚举式对象的属性 - let thing: EnumLikeObject = 'foo'
  3. 枚举式对象和类型必须具有同名
  4. 在enum样式对象的声明和类型中,没有字符串文字可以重复两次以上。如果你有一个解决方案,他们只能重复一次,甚至更好。 (在这个问题中,当我谈到冗长时,这个标准主要是我所说的。)
  5. 异议&讨论

    • issue #15930 link from Option 3 about string enums and string literals说“理性就是如果你使用字符串枚举,你应该一直使用它们来确保安全重构,否则坚持使用字面类型”
      • 在我们的项目中,我们对一些xml数据使用解析库(我们不控制数据格式)。这为我们提供了一个使用字符串文字类型的类型化对象,我们将其映射到使用这些字符串枚举的内部使用的对象。 (因此有关字符串文字的要求。)有时,我们从内部对象到生成xml的另一种方式工作,为了便于在这些情况下使用,我们想要枚举而不仅仅是文字类型。
      • 我可能会考虑更改该解析库的类型定义以使用我们的字符串枚举而不是文字然后我可以删除字符串文字分配要求,但我宁愿避免这样做,因为该库不是我们的责任,从外部库中使用我们的内部类型似乎很麻烦。
    • issue #16389 from @tycho's comment说“任何不是常数的字面值都会被预测会改变,但不是在类型中”这就是为什么你需要告诉编译器你是否知道变量实际上不会改变的原因在某种范围之外的类型。编译器必须推断出最常规的类型以允许更改值。

2 个答案:

答案 0 :(得分:5)

如果你想等待并忍受一点,

TypeScript 2.4将真正的字符串枚举带到比赛场地:

enum TwoWords {
  Foo = 'foo',
  Bar = 'bar'
}

function DoSomething(word: TwoWords) {
    switch (word) {
        case TwoWords.Foo:
            break;
        case TwoWords.Bar:
            break;
        default:
            let typeCheck: never = word; // OK
    }
}

这可以让你获得两全其美。

答案 1 :(得分:2)

我想我明白了。至少:

  • 我的代码中没有看到任何红色下划线。
  • DoSomething接受"foo""bar"
  • 在默认情况下,word无效。

并满足您的标准

  1. TwoWords.Foo === 'foo'
  2. 该类型仅允许指定的文字值。
  3. 枚举样式对象和类型具有相同的名称
  4. 字符串文字只写一次
  5. 所以不用多说:

    const TwoWords = (function () {
        const Foo = 'foo';
        const Bar = 'bar';
    
        const ret = {
            Foo: Foo as typeof Foo,
            Bar: Bar as typeof Bar,
        };
        return ret;
    })()
    type TwoWords = typeof TwoWords[keyof typeof TwoWords];
    

    然后我有一个灯泡时刻

    namespace TwoWords2 {
        export const Foo = "foo";
        export const Bar = "bar";
    }
    type TwoWords2 = typeof TwoWords2[keyof typeof TwoWords2]
    // didn't test this, not sure if it actually updates the 
    // original object or just returns a frozen copy
    Object.freeze(TwoWords2); 
    

    在我看来,这并不是一个缺点,因为它仍然在类型检查器和VS代码中引发错误,但TwoWords2.Bar = "five"实际上是有效的,因为命名空间被编译为一个简单的对象。但这就是打字稿的工作方式。显然第一个代码也有这个问题,但它不会抛出类型错误,所以第二个代码是优越的,IMO。