【问题标题】:How do I get TypeScript lookup types to work in this situation?如何让 TypeScript 查找类型在这种情况下工作?
【发布时间】:2020-09-03 06:36:23
【问题描述】:

很抱歉这个没有动机的标题,但我真的不知道还能叫什么。

我有一个枚举,我想将枚举的每个条目与一个类型相匹配。我这样做是为了让函数的参数可以根据第一个参数动态变化。一个例子:

enum Enum {
    A,
    B,
    C
}

interface TypeMap {
    [Enum.A]: number,
    [Enum.B]: string,
    [Enum.C]: boolean
}

function doSomething<K extends Enum>(a: K, b: TypeMap[K]) {
    //
}

调用 doSomething 时类型系统正常工作,例如:doSomething(Enum.A, 5) 有效,但 doSomething(Enum.A, "hello") 无效。但是,我无法工作的是:

function doSomething<K extends Enum>(a: K, b: TypeMap[K]) {
    if (a === Enum.A) {
        let num: number = b;
    }
}

在分配给num 时出现打字稿错误,但很明显,很明显,它应该可以工作。如果aEnum.A,那么b 必须定义为number,对吗?我究竟做错了什么?我如何让它发挥作用?

【问题讨论】:

    标签: typescript generics lookup mapped-types


    【解决方案1】:

    我可能已经找到了使用类型谓词的解决方案。 作为进行类型检查的经典方法(a === Enum.A)的替代方法,我们可以定义一个函数,在该函数中我们同时传递 A 和 B,并通过使用类型谓词(基本上关键字)。像这样:

    function isA(a: Enum, b: any): b is TypeMap[Enum.A] {
      return a === Enum.A
    }
    
    //function isB
    //function isC
    
    function doSomething<K extends Enum>(a: K, b: TypeMap[K]) {
      if (isA(a, b)) {
          let x: number = b; 
      }
    }
    

    这样做可以让编译器成功地缩小B的类型。

    您可以通过this link查看此内容。

    【讨论】:

    • 哦,哇,我不知道这是可能的。这是相当强大的!它说 b 现在是“TypeMap[K] & number”类型,我想这很好吗?
    • 是的,我想应该没问题。它基本上是 (string | number | boolean) & number 之间的交集类型,显然是“number”类型
    【解决方案2】:

    这里的主要问题是控制流分析只能缩小联合类型的类型。它不会导致扩展联合类型的泛型类型参数变窄。仅仅因为您测试了a,类型为K extends Enum,它不会缩小K 本身。 GitHub 中有一个关于此的未解决问题:microsoft/TypeScript#24085

    在检查a 时缩小K 的一个问题是,没有什么能阻止K 成为完整的联合类型Enum。例如:

    function getEnum(): Enum { return Enum.A };
    

    如果我调用getEnum(),我肯定会在运行时得到Enum.A,但编译器只将返回类型视为Enum,即完整的联合类型。因此,如果您调用doSomething(),编译器允许这样做:

    doSomething(getEnum(), "oops"); // no error!
    

    哎呀。所以实际上doSomething() 实现中的错误实际上是在警告你一个真正的(如果不常见的)问题:K 可能是Enuma 可能是Enum.A,而b 可能是什么除了number

    如果您可以告诉编译器 K 被限制为 恰好是 Enum 联合的一个成员,那么进行缩小会更安全。目前还没有办法表达这种通用约束,但有一个未解决的问题要求它:microsoft/TypeScript#27808

    现在你必须通过放弃一些编译器保证的类型安全来解决这个问题,例如使用type assertions

    function doSomethingAssert<K extends Enum>(a: K, b: TypeMap[K]) {
      if (a === Enum.A) {
        let num = b as number; // assert here
      } else if (a === Enum.B) {
        let str = b as string; // assert here
      } 
    }
    

    这可能是对您来说破坏性最小的解决方案。您可以使用其他解决方法,例如the other answer 中的用户定义类型保护,但它同样缺乏类型安全性(没有什么可以阻止您编写let num = b as string,也没有什么可以阻止您在类型保护中编写isA(a, "oops")。所以它是取决于你更喜欢哪种不健全的味道。


    最后一个想法:也许您会考虑重构您的数据以使用discriminated union 而不是一对函数参数?它不再是通用的吗?编译器在区分联合对象上使用控制流分析要好得多。因此,您可以将 ab 打包成一个对象类型,如下所示:

    type DiscrimUnion = { [K in Enum]: { a: K, b: TypeMap[K] } }[Enum]
    // type DiscrimUnion = { a: Enum.A; b: number;} | { a: Enum.B; b: string;} | 
    //   { a: Enum.C; b: boolean;}
    

    然后实现按照你想要的方式工作:

    function doSomethingDiscrimUnion(u: DiscrimUnion) {
      if (u.a === Enum.A) {
        let num: number = u.b;
      } else if (u.a === Enum.B) {
        let str: string = u.b;
      }
    }
    

    并且对如何调用它有更好的保证:

    doSomethingDiscrimUnion({a: Enum.A, b: 123}); // okay
    doSomethingDiscrimUnion({a: getEnum(), b: "oops"}); // error! not a DiscimUnion
    

    好的,希望对您有所帮助;祝你好运!

    Playground link to code

    【讨论】:

    • 好的,谢谢。老实说,这有点令人难过,因为用例甚至没有那么复杂,但 TypeScript 无法正确处理它。我真的希望他们有一天会实现泛型类型缩小。有区别的联合解决方案是更可取的解决方案,因为它提供了我想要的 100% 类型安全性。但是有点糟糕,我现在必须用一个对象调用函数......
    猜你喜欢
    • 2020-06-18
    • 1970-01-01
    • 1970-01-01
    • 2021-05-25
    • 1970-01-01
    • 1970-01-01
    • 2020-02-27
    • 1970-01-01
    • 2018-06-16
    相关资源
    最近更新 更多