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

Difference between covariant and contravariant positions in Typescript

我试图从 Typescript advanced types handbook.

中理解以下示例

引用,它说:

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

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

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

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

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

另外,第二个示例似乎解析为 never 不确定是否需要任何配置才能使其正常工作。

您观察到其中一个示例解析为 never 是准确的,您没有遗漏任何编译器设置。在较新版本的 TS 中,原始类型的交集解析为 never。如果您恢复为 older version,您仍然会看到 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;}

Playground Link

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

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

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 

Playground Link

由于 Fn<Animal>Fn<Dog> 可以在相反方向赋值,因为 DogAnimal 类型的两个变量将是,函数参数位置使 Fn T

中的逆变

对于属性,关于它们为什么是协变的讨论有点复杂。 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

Playground Link

您可能还会发现我的 post 关于 TS 方差的有趣信息。