Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Difference between covariant and contravariant positions in Typescript

I'm trying to understand the following examples from the Typescript advanced types handbook.

Quoting, it says that:

The following example demonstrates how multiple candidates for the same type variable in co-variant positions causes a union type to be inferred:

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

Likewise, multiple candidates for the same type variable in contra-variant positions causes an intersection type to be inferred:

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

My question is: why are the object properties from the first example considered "co-variant positions" while the second function arguments are considered "contra-variant positions"?

Also the second example seems to resolve to never not sure if there's any config required to get it to work.

like image 815
ᴘᴀɴᴀʏɪᴏᴛɪs Avatar asked Jun 21 '20 08:06

ᴘᴀɴᴀʏɪᴏᴛɪs


1 Answers

Your observation that one of the examples resolves to never is accurate and you are not missing any compiler settings. In newer versions of TS, intersections of primitive types resolve to never. If you revert to an older version you will still see string & number. In newer version you can still see the contravariant position behavior if you use object types:

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

As to why are function parameters contravariant while properties are covariant, it's a tradeoff between type safety and usability.

For function arguments, it is easy to see why they would be contravariant. You can only call a function safely with a subtype of the argument, not a base type.

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

Since Fn<Animal> and Fn<Dog> are assignable in the opposite direction as two variables of types Dog and Animal would be, the function parameter position makes Fn contravariant in T

For properties, the discussion as to why they are covariant is a bit more complicated. The TL/DR is that a field position (ex { a: T }) would make the type actually invariant, but that would make life hard so in TS, by definition a field type position (such as T has above) makes the type covariant in that field type( so { a: T } is covariant in T). We could demonstrate that for the a is read-only case, { a: T } would covariant, and for the a is write-only case { a: T } would be contravariant, and both cases together give us invariance, but I'm not sure that is strictly necessary, instead, I leave you with this example of where this covariant by default behavior can lead to correctly typed code having runtime errors:

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

You might also find this post of mine on variance in TS interesting.

like image 172
Titian Cernicova-Dragomir Avatar answered Sep 30 '22 02:09

Titian Cernicova-Dragomir