【问题标题】:function with unknown parameter is not assignable to function with string parameter具有未知参数的函数不能分配给具有字符串参数的函数
【发布时间】:2020-03-20 09:30:14
【问题描述】:

我正在使用打字稿,我定义了以下接口:

export interface BaseProtocol {
    [key: string]: (payload: unknown) => Promise<unknown>;
}

现在我想定义一个协议:

import { BaseProtocol } from './BaseProtocol';

export interface AppProtocol extends BaseProtocol {
    action(payload: string): Promise<string>;
}

不幸的是,我收到以下错误:

Property 'action' of type '(payload: string) => Promise<string>' is not assignable to string index type '(payload: unknown) => Promise<unknown>'.ts(2411)

我不愿意使用任何,因为它损害了协议使用的类型安全。 知道如何解决这个问题吗?

【问题讨论】:

  • type AppProtocol = { action(payload: string): Promise&lt;string&gt;; } &amp; BaseProtocol;
  • 有什么区别?
  • 这是一个 TS 类型系统的怪癖,不确定它是否记录在任何地方,但应该这样做。
  • 但是这给了我一个错误:const a:Protocol = { action(payload: string){ return new Promise&lt;string&gt;((resolve)=&gt;resolve() ); };: Type 'unknown' is notassignable to type 'string'。
  • 哦,那真是令人惊讶,它应该可以工作。让我们看看..

标签: typescript typescript-generics


【解决方案1】:

错误是有效的;如果你有接口AB extends A,那么如果我想要A,你可以给我B。但是根据您的定义,这会中断:

function callBaseProtocol(bp: BaseProtocol) {
    return bp.action(12345);
}

function callAppProtocol(ap: AppProtocol) {
    callBaseProtocol(ap); // oops!
}

函数callBaseProtocol() 没有错误。 BaseProtocol 的定义表明 每个 字符串值键是一个函数,它接受 unknown 类型的值并返回 Promise&lt;unknown&gt;。具体来说,BaseProtocolaction 属性必须是接受unknown 类型值的函数。因此,您可以将其传递给number

函数callAppProtocol() 也没有出错。在这里,我们调用callBaseProtocol() 并将其传递给AppProtocol。这应该是可以接受的,因为AppProtocol extends BaseProtocol

但是如果AppProtocol 有一个action 属性,它需要一个string 并在其上调用一些string 特定的方法,比如x =&gt; x.toUpperCase(),这将会爆炸。错误在于AppProtocol 实际上并没有正确扩展BaseProtocol


这种可能令人惊讶的现象被称为parameter contravariance,如果XY 的子类型,那么函数(y: Y)=&gt;any(x: X)=&gt;any 的子类型,反之亦然时间>。子类型化的方向对于函数参数是相反的,而“相反”就是“逆变”的意思。

这种行为是通过 --strictFunctionTypes compiler option 在 TypeScript 中引入的,是一种类型安全改进。


那么,你该如何处理呢?假设您想要提高类型安全性而不是降低类型安全性(您提到不想使用any),那么您的BaseProtocol 可能没有真正正确定义。也许不是有一个字符串索引签名和传递unknowns,它应该是一个generic接口,表示输入和输出之间更复杂的关系。也许是这样的:

export type BaseProtocol<T> = {
    [K in keyof T]: (payload: T[K]) => Promise<T[K]>;
}

这是说BaseProtocol 依赖于另一个类型T,并且BaseProtocol&lt;T&gt;T 的每个属性都有一个方法,该方法接受该属性值类型的参数,并返回该类型的Promise

然后AppProtocol可以简单定义为:

export interface AppProtocol extends BaseProtocol<{ action: string }> {
}

这解决了之前的问题,因为编译器不允许您在 number 参数上调用 BaseProtocol&lt;T&gt;action() 方法,除非它知道 T 具有类型为 action 的属性number:

function callBaseProtocol<T>(bp: BaseProtocol<T>) {
    return bp.action(12345); // error! might not have an action
}

还有一种混合方法,它不假设每个函数的返回值是与其输入相同类型的承诺:

export type BaseProtocol<T> = {
    [K in keyof T]: (payload: T[K]) => unknown;
}

export interface AppProtocol extends BaseProtocol<{ action: string }> {
    action(x: string): Promise<string>;
}

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

Playground link to code

【讨论】:

  • 非常感谢您的解释。实际上,我之所以需要 BaseProtocol,是因为我有一个使用 Parameters 和 ReturnType 的sendMssage&lt;Protocol extends BaseProtocol&gt;(name: keyof Protocol, payload: Parameters&lt;keyof Protocol&gt;[0]) 方法。顺便说一句,返回类型不同于使一切变得复杂的有效负载。
猜你喜欢
  • 2022-01-21
  • 2020-11-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-09-20
  • 2021-11-02
  • 2021-10-28
相关资源
最近更新 更多