Typescript中协变和逆变位置的区别

ᴘᴀɴ*_*ᴛɪs 5 covariance typescript

我试图从Typescript 高级类型手册中了解以下示例。

引用,它说:

以下示例演示了协变位置中同一类型变量的多个候选者如何导致推断联合类型:

type Foo<T> = T extends { a: infer U, b: infer U } ? U : never;
type T10 = Foo<{ a: string, b: string }>;  // string
type T11 = Foo<{ a: string, b: number }>;  // string | number
Run Code Online (Sandbox Code Playgroud)

同样,逆变位置中同一类型变量的多个候选会导致推断出交叉类型:

type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
type T20 = Bar<{ a: (x: string) => void, b: (x: string) => void }>;  // string
type T21 = Bar<{ a: (x: string) => void, b: (x: number) => void }>;  // string & number
Run Code Online (Sandbox Code Playgroud)

我的问题是:为什么第一个示例中的对象属性被视为“协变位置”,而第二个函数参数被视为“逆变位置”?

此外,第二个示例似乎解决了永远不确定是否需要任何配置才能使其工作的问题。

Tit*_*mir 8

您对其中一个示例解析的观察结果never是准确的,并且您没有丢失任何编译器设置。在较新版本的 TS 中,基本类型的交集解析为never。如果您恢复到旧版本,您仍然会看到string & number. 在较新的版本中,如果您使用对象类型,您仍然可以看到逆变位置行为:

type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
type T21 = Bar<{ a: (x: { h: string }) => void, b: (x: { g: number }) => void }>;  // {h: string; } & { g: number;}
Run Code Online (Sandbox Code Playgroud)

游乐场链接

至于为什么函数参数是逆变的,而属性是协变的,这是类型安全性和可用性之间的权衡。

对于函数参数,很容易理解为什么它们是逆变的。您只能使用参数的子类型(而不是基类型)安全地调用函数。

class Animal { eat() { } }
class Dog extends Animal { wof() { } }

type Fn<T> = (p: T) => void
var contraAnimal: Fn<Animal> = a => a.eat();
var contraDog: Fn<Dog> = d => { d.eat(); d.wof() }
contraDog(new Animal()) // error, d.wof would fail 
contraAnimal = contraDog; // so this also fails

contraAnimal(new Dog()) // This is ok
contraDog = contraAnimal; // so this is also ok 

Run Code Online (Sandbox Code Playgroud)

游乐场链接

由于Fn<Animal>和可以作为和Fn<Dog>类型的两个变量以相反的方向赋值,因此函数参数位置在DogAnimalFnT

对于属性,关于为什么它们是协变的讨论有点复杂。TL/DR 是,字段位置(例如{ a: T })将使类型实际上保持不变,但这会使生活变得困难,因此在 TS 中,根据定义,字段类型位置(如T上面的)使该字段类型中的类型协变( ){ a: T }中的协变也是如此T。我们可以证明,对于a只读情况,{ a: T }将是协变的,而对于a只读情况,{ a: T }将是逆变的,这两种情况一起给我们带来不变性,但我不确定这是绝对必要的,相反,我离开通过这个示例,您可以看到默认情况下的协变行为可能会导致正确键入的代码出现运行时错误:

type SomeType<T> = { a: T }

function foo(a: SomeType<{ foo: string }>) {
    a.a = { foo: "" } // no bar here, not needed
}
let b: SomeType<{ foo: string, bar: number }> = {
    a: { foo: "", bar: 1 }
}

foo(b) // valid T is in a covariant position, so SomeType<{ foo: string, bar: number }> is assignable to SomeType<{ foo: string }>
b.a.bar.toExponential() // Runtime error nobody in foo assigned bar

Run Code Online (Sandbox Code Playgroud)

游乐场链接

您可能还会发现我关于 TS 方差的这篇文章很有趣。