typescript - 如何让 TypeScript 查找类型在这种情况下工作?

标签 typescript generics lookup mapped-types

抱歉,这个标题没有动机,但我真的不知道该怎么调用它。

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

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;
    }
}

Typescript 在分配给 num 时出错,但很明显,很明显,它应该可以工作。如果 aEnum.A,那么 b 根据定义必须是 number,对吧?我究竟做错了什么?我如何让它发挥作用?

最佳答案

这里的主要问题是控制流分析只能缩小联合类型的类型。它不会导致扩展联合类型的泛型类型参数变窄。仅仅因为您已经测试了类型为 K extends Enuma,它不会缩小 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.Ab 可以是 number 以外的东西。

如果您可以告诉编译器 K 被限制为 恰好是 Enum 联合的一个成员,那么它会更安全做缩小。现在没有办法表达这种通用约束,但有一个 Unresolved 问题需要它: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 - 如何让 TypeScript 查找类型在这种情况下工作?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61853054/

相关文章:

javascript - 如何将扩展导入 ES6/Typescript 模块

angular - ng2-charts + 如何自定义X轴标签的位置?

c# - 如何获得扩展方法来更改原始对象?

ios - swift 3 : Cannot invoke (protocol method) with argument list of type (generic)

python - 使用向量查找的 Tensorflow 变换张量

knockout.js - 没有包装器的 Knockout Typescript 传递对象

reactjs - React.js - Redux 钩子(Hook)无效的钩子(Hook)调用

swift - Comparable 协议(protocol)可以通用吗?

c# - 用于键值查找的更简单的数据结构?

ejb - 如何在 JBoss AS 7.x 上查找 ejb