typescript 为什么函数类型的参数不能接受一个参数实现了原始参数的接口的参数?

xnifntxz  于 2023-01-27  发布在  TypeScript
关注(0)|答案(1)|浏览(157)

好吧,这个标题很奇怪,但奇怪的是,你不能传递一个函数,它接受的类型实现了传入函数应该接受的接口:

interface Empty { }

class Implements implements Empty {
    constructor(public s: string) {}
}

type TakesEmpty = (e: Empty) => void;

function TakesImplementor(e: Implements): void {}

function f(t: TakesEmpty) { }

/*
  Argument of type '(e: Implements) => void' is not assignable to parameter of type 'TakesEmpty'.
  Types of parameters 'e' and 'e' are incompatible. Property 's' is missing in type 'Empty' but required in type 'Implements'.
*/
f(TakesImplementor);

// But this works
function te (e: Empty): void {}
te(new Implements(""));

Playground链接
为什么会这样呢?

nzk0hqpo

nzk0hqpo1#

在你的示例代码中,运行时不会发生什么坏事,因为函数什么都不做,但是给定TakesImplementor()的签名,它可以像这样实现:

function TakesImplementor(e: Implements): void {
    console.log(e.s.toUpperCase());
}

TakesImplementor()接受一个Implements类型的名为e的参数,该参数有一个string类型的s属性,在TakesImplementor()中可以访问e.s.toUpperCase()
然后,给定f()的签名,它可以像这样实现:

function f(t: TakesEmpty) {
    t({});
}

传入f()t参数本身就是TakesEmpty类型的函数,这意味着您应该能够调用t({}),因为对象{}是有效的Empty
是的,一切都很好,直到你开始运行这个:

f(TakesImplementor);

f(TakesImplementor)调用TakesImplementor({})TakesImplementor({})调用{}.s.toUpperCase(){}.s.toUpperCase()并不存在,因为{}.s是未定义的。这就是f(TakesImplementor)行给出错误消息的原因:

// Property 's' is missing in type 'Empty' but required in type 'Implements'.

TakesImplementor不是TakesEmpty,因为TakesImplementor只接受Implements类型,而TakesEmpty需要接受所有Empty类型,而不仅仅是Implements类型。
这可能是违反直觉的,但在处理函数时,当你 * 加宽 * 它的参数类型时,你 * 缩小 * 它的类型,当你 * 缩小 * 它的参数类型时,你 * 加宽 * 它的类型。函数参数的这种“走另一条路”被称为 * convariance *。它们以相反的方式变化,或者说“contra-variant”。(这与函数 return 类型相反,它在加宽时加宽函数类型,在缩小时缩小函数类型......它们一起变化,或“共变”,因此是 * 协变 *。)
通过将TakesImplementor的参数类型从Empty缩小到Implements,您已经 * 加宽 * 了TakesImplementor本身的类型(与TakesEmpty相比)。每个TakesEmpty都可以用于替换TakesImplementor,但反之亦然。
当你使用--strictFunctionTypes编译器标志时,函数参数类型的反变在TypeScript 2.6和更高版本中被强制执行(同样只在--strict中被启用)。
链接到代码

相关问题