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

Posted

技术标签:

【中文标题】如何使 TypeScript 字符串枚举适用于字符串文字并正确进行类型推断【英文标题】:How to make TypeScript string enums that work with string literals and do type inference correctly 【发布时间】:2017-11-24 19:47:18 【问题描述】:

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

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

const TwoWords 被初始化为所有键的值类型断言为TwoWords,然后DoSomething(TwoWords.Foo) 编译,但switch 语句上的类型保护不能按预期工作-word 的类型在默认大小写不是never

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');

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

但是,如果我对每个 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''bar' 是更长的字符串(比如说,一个完整的句子),我不想复制它们——这太冗长了。是否有另一种方法可以让字符串键枚举在 switch 语句(或 if/else 链)中从类型推断的角度表现如预期?

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

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

DoSomething('bar'); // Type '"bar"' is not assignable to parameter of type 'TwoWords'

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

标准

我正在寻找另一个可以让我拥有的选项:

    一个枚举样式对象,用于访问字符串文字EnumLikeObject.Foo === 'foo' 一个 type 表示只允许枚举成员,无论它们是:
      字符串字面量 - let thing: EnumLikeObject = 'foo' 枚举样式对象的属性 - let thing: EnumLikeObject = EnumLikeObject.Foo
    枚举样式对象和类型必须具有相同的名称 在枚举样式对象和类型的声明中,字符串文字不能重复超过两次。如果您有一个只能重复一次的解决方案,那就更好了。 (在这个问题中,当我谈到冗长时,这个标准主要是我所指的。)

异议与讨论

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

【问题讨论】:

这似乎归结为无需编写'foo' as 'foo' 即可获得'foo' 推理的能力。这似乎是#16389的话题。 感谢@Tycho 的链接。是的,如果foo 的类型被推断为'foo' 而不是string,那将使我的“选项2”不那么冗长并解决问题。正如那个链接的 GitHub 问题中所解释的,这是一个可变性问题。 const foo = 'foo'; 的类型为 'foo',而 let foo = 'foo' 的类型为 string;不知道如何利用它来解决这个问题。有什么想法吗? 哈,如果你用const 存储字符串,我什至不知道这已经奏效了!所以我想这意味着剩下的就是让他们将该行为扩展到对象(和数组)。在那之前,如果你说const Foo = 'foo'; const Bar = 'bar'; const TwoWords = Foo, Bar ; 会怎样?仍然不完美,但是是的。 令人惊讶的是,这也不起作用(只是尝试过)。属性Foo 的类型为'foo',但TwoWords 的类型为 Foo: string, Bar: string 哎哟......也许订阅那个问题,希望他们会考虑在某一时刻支持这一点。如果有人为赏金创造奇迹,我也会喜欢这里。 【参考方案1】:

如果你愿意等待和忍受一点,

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
    

这让你两全其美。

【讨论】:

太棒了!是的,我可以更新到 2.4,并且几乎可以。但这实际上还不够,因为我需要能够将字符串文字分配给使用此字符串键控枚举键入的变量。我确实在我的要求的第 2 点中提到了这一点,但我可以看到它并不完全清楚。我更新了示例以包含 DoSomething('bar'),它不适用于 TypeScript 2.4 字符串键控枚举。有什么建议吗? 嗯,这实际上似乎是 TypeScript 中的一个错误,我希望它可以工作...... 很遗憾,好像是by design and not a bug。【参考方案2】:

我想我明白了。至少:

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

为了满足你的标准

    TwoWords.Foo === 'foo' 该类型只允许指定的文字值。 枚举样式对象和类型同名 字符串文字只写一次

废话不多说:

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 Code 中引发错误,但 TwoWords2.Bar = "five" 确实有效,因为命名空间被编译为一个简单的对象。但这就是打字稿的工作方式。显然第一个代码也有这个问题,但它不会抛出类型错误,所以第二个更好,IMO。

【讨论】:

真漂亮 看起来let thing = TwoWords.Bar; DoSomething(thing); 不适用于此解决方案,但它适用于我的问题中的“选项2” - 但我认为没有任何方法可以解决这个问题。 let thing = 'bar'; DoSomething(thing); 也不能按照问题 #16389 工作 老实说,您不应该将文字类型分配给 let 变量。如果你以后改变它会发生什么?你最终会得到"foo" as "bar"!如果您使用 const ,它将按照您想要的方式进行。 const thing = TwoWords.Bar; DoSomething(thing); 如果你真的想这样做,那么你必须告诉 typescript 这就是你想要的:let me: typeof TwoWords2.Bar = TwoWords2.Bar;。但我不推荐它,因为你会冒着破坏类型检查的风险,而const 工作得很好。 是的,完全正确。这就是他们在 issue #16389 中所说的——如果你知道它比字符串更具体,你需要明确地告诉编译器。感谢您的帮助

以上是关于如何使 TypeScript 字符串枚举适用于字符串文字并正确进行类型推断的主要内容,如果未能解决你的问题,请参考以下文章

在Angular 6模板语句中将字符串转换为Typescript枚举

用于将字符串枚举成员转换为枚举的Typescript函数

使用字符串类型参数访问枚举时出现 TypeScript TS7015 错误

LayaBox---TypeScript---枚举

如何使 PhpStorm 在 .vue 文件中反映 TypeScript tsconfig.json?

typeScript中的数据类型