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

9
我在尝试理解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

同样地,协变位置上相同类型变量的多个候选项会导致推断出一个交集类型:
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,不确定是否需要任何配置才能使其工作。

这意味着它的解析结果为“string | number”,而不是指定的“string & number”。 - ᴘᴀɴᴀʏɪᴏᴛɪs
1
你说得对!我其实不知道我在说什么。有一位TypeScript工程师提交了一个PR,阐述了协变和逆变在方法签名中的思考,但它只涉及到方法,所以并没有回答你的问题。 - GOTO 0
感谢这个PR,它非常有用。此外,我猜测示例无法工作的原因是 这个 - ᴘᴀɴᴀʏɪᴏᴛɪs
1个回答

8
您观察到其中一个示例解析为never是准确的,并且您没有错过任何编译器设置。在较新版本的TypeScript中,原始类型的交集会解析为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;}

Playground链接

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

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

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链接

对于属性而言,它们为何是协变的讨论会更加复杂。简而言之,一个字段位置(例如{ a: T })会使类型实际上不变,但这会使生活变得困难,因此在TS中,根据定义,字段类型位置(如上述的T)使类型在该字段类型上是协变的(因此{ a: T }T上是协变的)。我们可以证明对于只读的a的情况,{ a: T }会是协变的,在写入情况下,{ a: T }将是反变的,这两种情况加在一起,给我们得到不变性。但我不确定这是严格必要的,相反,我留下了这个例子,说明默认情况下的协变行为会导致正确的类型代码出现运行时错误:

由于Fn<Animal>Fn<Dog>在互相赋值的方向上与类型为DogAnimal的两个变量相同,函数参数位置使FnT上是反变的。

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 链接

你可能也会发现我在 TS 中关于 variance 的 这篇 帖子很有趣。


非常有趣,谢谢!对我来说,属性是协变的这一点似乎很荒谬,但是你的观点“在TS中会让生活变得困难”,为我解决了这个问题。 - ᴘᴀɴᴀʏɪᴏᴛɪs
这对于任何不了解“方差”概念的人都可能有所帮助:https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science) - aderchox
此外,还有关于方差的讲座,链接为https://www.youtube.com/watch?v=EInunOVRsUU。 - Titian Cernicova-Dragomir

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接