【问题标题】:Argument of type 'string | number' is not assignable to parameter of type 'never''string | 类型的参数number' 不可分配给“从不”类型的参数
【发布时间】:2022-01-08 10:33:50
【问题描述】:

片段和错误:

const methods = {
  a(value: number) {},
  b(value: string) {}
};

function callMethodWithArg(methodAndArg: { method: 'a'; arg: number; } | { method: 'b'; arg: string; }) {
  methods[methodAndArg.method](methodAndArg.arg);
}

'string | 类型的参数number' 不可分配给“从不”类型的参数。 类型“字符串”不可分配给类型“从不”。

看起来 typescript 不够智能,无法确定方法 a 只能用数字调用,而方法 b 只能用字符串调用。

有什么建议可以正确输入吗?

Playground

【问题讨论】:

  • 这是我在ms/TS#30581 中一直称为“相关联合类型”的问题。目前只有变通方法,但是当 TypeScript 4.6 发布时,您应该能够稍微将此代码重构为 this 之类的东西以使其正常工作。你想让我把这个写下来作为答案吗?还是我错过了您的问题?
  • 谢谢@jcalz。如果没有ArgMap 接口,你能做到吗?它复制了 methods 变量中已经存在的信息。
  • 我认为你需要一个像ArgMap 这样的基本界面来映射;正是因为我们给了methods 一个分布在这样一个基本类型上的类型,所以相关性才起作用。事实上,如果你只是不注释methods 然后it fails,即使类型在两种情况下都是等价的。你想要一个解释情况的答案吗?
  • 我也注意到了,这似乎很奇怪。如果没有ArgMap,有一个更优雅的解决方案会很棒。请继续详细回答。
  • @jcalz 也许infer 能帮上忙?

标签: typescript


【解决方案1】:

这里的问题是 TypeScript 到 4.5 版本不支持我一直称之为“相关联合”的东西,正如 microsoft/TypeScript#30581 中所讨论的那样。正如您所说,编译器无法理解方法a 只能用number 调用,而方法b 只能用字符串调用。如果我们将单行分成几行来检查函数的类型及其参数,我们会看到每一行都是union type

type MethodAndArg = { method: 'a'; arg: number; } | { method: 'b'; arg: string; }

function callMethodWithArg(methodAndArg: MethodAndArg) {
    const f = methods[methodAndArg.method];
    // const f: ((value: number) => void) | ((value: string) => void)
    const arg = (methodAndArg.arg);
    // const arg: string | number
    f(arg) // error!
}

f 是函数的联合,arg 是参数类型的联合。如果不知道farg 是相关的(假设您从methodAndArgarg 获得了来自同一类型的不同对象的f),那么您当然不能调用联合具有参数类型联合的函数。也许fmethods.abnumber。编译器分别跟踪farg类型,就好像它们是独立的一样。它不跟踪他们的来源


在 TypeScript 4.5 及更低版本中,我知道处理此问题的唯一方法是编写类型安全但冗余的代码,如下所示:

function callMethodWithArg(methodAndArg: MethodAndArg) {
    if (methodAndArg.method === "a") {
        methods[methodAndArg.method](methodAndArg.arg);
    } else {
        methods[methodAndArg.method](methodAndArg.arg);
    }
}

您使用control flow analysismethodAndArg 缩小到每个可能的子类型,然后编译器可以在每种情况下验证函数调用。

或者,您可以编写简洁但不安全的代码,如下所示:

function callMethodWithArg(methodAndArg: MethodAndArg) {
    (methods[methodAndArg.method] as (value: number | string) => void)(
        methodAndArg.arg);
}

您使用type assertion 来假装该函数可以接受所有numberstring 输入,即使实际上它只会接受其中一个。这是不安全的,因为您可以传入 Math.random()<0.5 ? "a" : 1 而不是 methodAndarg.arg 并且不会出错。


在 TypeScript 4.6 及更高版本中,您应该能够使用 microsoft/TypeScript#47109 中介绍的称为“分布式对象类型”的东西。这个想法是想出一个映射对象类型,它表示从某个键名到函数参数的映射,然后使用这个映射对象类型来生成其他类型。只要根据映射类型正确表达事物,编译器就能够“看到”相关性。

您的示例如下所示:

// mapping type
interface ArgMap {
    a: number;
    b: string;
}

所以ArgMap 是映射类型。从某种意义上说,它最简单地捕获了类型关系(例如,“anumber 搭配,bstring 搭配”)。现在我们可以构建您的其他类型:

// map over ArgMap to get methods type
type Methods = { [K in keyof ArgMap]: (value: ArgMap[K]) => void }

const methods: Methods = {
    a(value: number) { },
    b(value: string) { }
};

Methods 类型是 mapped type 上的 ArgMap,我们 annotate methods 属于这种类型。这个注解很重要,否则methodsmethodAndArg的类型之间的链接就会断开。

下一个:

// map over ArgMap and index into it to get generic MethodAndArg type
type MethodAndArg<K extends keyof ArgMap = keyof ArgMap> =
    { [P in K]: { method: P, arg: ArgMap[P] } }[K]

这里我们有一个genericMethodAndArg&lt;K&gt; 类型,其中K 默认为keyof ArgMap。如果你只写MethodAndArg,它与以前的联合类型相同,但如果你写MethodAndArg&lt;"a"&gt;,它将只是a 方法/arg 对,MethodArg&lt;"b"&gt; 只是b 方法/arg 对.这就是我们需要的“分布式对象类型”。

最后,你的callMethodWithArg() 函数:

// make callMethodWithArg generic in the key type of ArgMap
function callMethodWithArg<K extends keyof Methods>(methodAndArg: MethodAndArg<K>) {
    methods[methodAndArg.method](methodAndArg.arg); // okay
}

这个函数在K 中是通用的,methodAndArg 的类型是MethodAndarg&lt;K&gt;。相关函数调用编译没有问题,没有任何多余的 JS 代码或任何 TS 类型断言。这既是类型安全的,又能生成简洁的 JS 代码。当您调用 callMethodWithArg() 时,编译器将(在 TS4.6+ 中,记住)正确推断 K

callMethodWithArg({ method: "a", arg: 123 }); // okay
// K inferred as "a"

callMethodWithArg({ method: "b", arg: "xyz" }); // okay
// K inferred as "b"

callMethodWithArg({ method: "a", arg: "xyz" }); // error! 
// K inferred as "a", but arg is wrong

所以这已经差不多了!


我发现您确实想要工作量更少的东西,编译器可以“看到”相关性而无需注释 methods。也许在某些未来版本的 TypeScript 中可能会发生这种情况,但目前我认为这是不可能的。

Playground link to code

【讨论】:

    【解决方案2】:

    我不确定是否有更清洁的解决方案,但如果您没有太多案例,这将起作用:

    const methods = {
      a(value: number) {},
      b(value: string) {}
    };
    
    function callMethodWithArg(methodAndArg: { method: 'a'; arg: number; } | { method: 'b'; arg: string; }) {
      if (methodAndArg.method === 'a') {
        // now it knows that method has to be 'a' and arg is a number
        methods[methodAndArg.method](methodAndArg.arg)
      } else if (methodAndArg.method === 'b') {
        methods[methodAndArg.method](methodAndArg.arg)
      }
    }
    

    Playground link

    【讨论】:

      【解决方案3】:

      我的解决方案是使用类型谓词将方法转换为形状 A 或 B(或者您需要的任意多个形状)

      type MethodA = (value: number) => void
      type MethodB = (value: string) => void
      
      type ArgA = { method: 'a'; arg: number; }
      type ArgB = { method: 'b'; arg: string; }
      
      type Methods = {
        [key: string]: MethodA | MethodB
      }
      
      const methods: Methods = {
        a(value: number) { },
        b(value: string) { }
      };
      
      
      function callMethodWithArg(methodAndArg: ArgA | ArgB) {
        const method = methods[methodAndArg.method]
      
        if (determinShape(methodAndArg)) {
          (method as MethodA)(methodAndArg.arg)
        } else {
          (method as MethodB)(methodAndArg.arg)
        }
      }
      
      
      /** Will cast methodAndArg as ArgA if true, else ArgB */
      function determinShape(methodAndArg: ArgA | ArgB): methodAndArg is ArgA {
        return Number.isFinite(methodAndArg.arg)
      }
      

      TS Playground

      【讨论】:

      • 我觉得很奇怪,determineShape 会检查有限。这样,如果我们通过Infinity,它会认为它是ArgB 类型并转到else 分支。例如,callMethodWithArg({ method: 'a', arg: Infinity }) 会调用methods.a(Infinity),但它会从 else 分支 (Playground link) 调用
      • 是的,这样做有点傻,但我想不出更好的方法。 isFinite vs isNaN:stackabuse.com/javascript-check-if-variable-is-a-number
      猜你喜欢
      • 2020-06-19
      • 2020-06-19
      • 1970-01-01
      • 2021-07-02
      • 2020-04-04
      • 2019-10-23
      • 1970-01-01
      • 2021-07-21
      • 1970-01-01
      相关资源
      最近更新 更多