考虑这个例子(现场示例):
interface Foo<T>
{
readonly fun: (v: T) => void; // causes error below
//fun(v: T): void; // works
}
function isNumber<T>(foo: Foo<T>): foo is Foo<T & number>
{
return true;
}
这会产生错误 2677:
A type predicate's type must be assignable to its parameter's type.
Type 'Foo<T & number>' is not assignable to type 'Foo<T>'.
Type 'T' is not assignable to type 'T & number'.
Type 'T' is not assignable to type 'number'.ts(2677)
但是如果我们将
readonly fun: (v: T) => void;
替换为成员函数 fun(v: T): void;
,问题就会消失。为什么这是一个问题,有没有办法在这里仍然使用 lambda 字段?
我们怀疑
foo
是一个(动态)字段,其类型稍后可以通过覆盖而更改,因此 lambdas 参数的 T
与 T
上下文中的 Foo<T & number>
不同。由于成员函数可能具有更严格的类型限制,因此它会收到正确的 T & number
类型。
--strictFunctionTypes
编译器标志启用时,函数的参数是逆变(请参阅TypeScript中的方差、协方差、逆变、双变量和不变性之间的差异)。 这意味着如果 T extends U
那么 ((x: U) => void) extends ((x: T) => void)
,而不是相反。注意方向的变化。 如果您尝试想象函数参数本身是数据流方向与您通常对类型的看法不同的变化,那么这是有道理的。您谈论的是数据被“接受”,而不是“产生”。狗是动物,猫也是动物。但如果我把我的狗带到一位“动物医生”那里,而这位“动物医生”因为只对猫有效而拒绝提供帮助,那么“猫是动物”这一事实并不会让情况变得更容易接受。真正的动物医生接受所有动物。每个动物医生都是猫医生,但不是每个猫医生都是动物医生。
无论如何,你的 Foo<T>
类型与 T
是逆变的。 因此,您没有理由尝试将
Foo<T>
缩小为
Foo<T & number>
。 每个Foo<T>
都是自动一个
Foo<T & number>
,就像每个动物医生自动都是猫医生一样:
function toNumber<T>(foo: Foo<T>): Foo<T & number> {
return foo; // okay
}
从Foo<T>
移动到
Foo<T & number>
是
扩大,而不是缩小。如果你尝试创建一个实际上变宽的
用户定义的类型保护函数,TypeScript 会抱怨你做错了:
function isString(x: "a" | "b"): x is string { // error!
// ~~~~~~~~~~~
return true;
}
你不能“缩小”"a" | "b"
到
string
,也不能“缩小”
Footo
Foo`。所以这就是发生这种情况的原因。如果您从
{a: (x: T) => void}
等函数类型切换到 {a(x: T): void}
等
method类型,TypeScript 使用 bivariance 来测量参数,并且错误就会消失。双方差是不安全的,但有时很有用(例如,请参阅[https://stackoverflow.com/q/60905518/2887218])。但除非您愿意编写不安全的代码,否则我认为您应该退后一步,弄清楚您真正在做什么,也许还需要重构。 但这超出了所提出问题的范围。Playground 代码链接