Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Difference between Variance, Covaraince, Contravariance and Bivariance in TypeScript

Could you please explain using small and simple TypeScript examples what is Variance, Covariance, Contravariance and Bivariance?

[CONTINUOUS UPDATE]

Useful links:

  1. Another one good answer of Oleg Valter related to the topic

  2. Very good explanation of*-riance by Titian-Cernicova-Dragomir

  3. Stephan Boyer blog

  4. Scala documentation - good explanation with examples

  5. @Titian's answer 1

  6. @Titian's answer 2

  7. Vlad Riscutia 's blog

  8. Mark Seemann 's article

  9. @jcalz explanation

like image 828
captain-yossarian Avatar asked Feb 28 '21 14:02

captain-yossarian


People also ask

What is the difference between covariance and contravariance?

In C#, covariance and contravariance enable implicit reference conversion for array types, delegate types, and generic type arguments. Covariance preserves assignment compatibility and contravariance reverses it.

What is covariance in TypeScript?

Here's a definition of covariance: A type T is covariant if having S <: P , then T<S> <: T<P> . The covariance of a type is intuitive. If Admin is a subtype of User , then you can expect Promise<Admin> to be a subtype of Promise<User> . Covariance holds for many types in TypeScript.


1 Answers

Variance has to do with how a generic type F<T> varies with respect to its type parameter T. If you know that T extends U, then variance will tell you whether you can conclude that F<T> extends F<U>, conclude that F<U> extends F<T>, or neither, or both.


Covariance means that F<T> and T co-vary. That is, F<T> varies with (in the same direction as) T. In other words, if T extends U, then F<T> extends F<U>. Example:

  • Function or method types co-vary with their return types:

    type Co<V> = () => V;
    function covariance<U, T extends U>(t: T, u: U, coT: Co<T>, coU: Co<U>) {
      u = t; // okay
      t = u; // error!
    
      coU = coT; // okay
      coT = coU; // error!
    }
    

Other (un-illustrated for now) examples are:

  • objects are covariant in their property types, even though this not sound for mutable properties
  • class constructors are covariant in their instance types

Contravariance means that F<T> and T contra-vary. That is, F<T> varies counter to (in the opposite direction from) T. In other words, if T extends U, then F<U> extends F<T>. Example:

  • Function types contra-vary with their parameter types (with --strictFunctionTypes enabled):

    type Contra<V> = (v: V) => void;
    function contravariance<U, T extends U>(t: T, u: U, contraT: Contra<T>, contraU: Contra<U>) {
      u = t; // okay
      t = u; // error!
    
      contraU = contraT; // error!
      contraT = contraU; // okay
    }
    

Other (un-illustrated for now) examples are:

  • objects are contravariant in their key types
  • class constructors are contravariant in their construct parameter types

Invariance means that F<T> neither varies with nor against T: F<T> is neither covariant nor contravariant in T. This is actually what happens in the most general case. Covariance and contravariance are "fragile" in that when you combine covariant and contravariant type functions, its easy to produce invariant results. Example:

  • Function types that return the same type as their parameter neither co-vary nor contra-vary in that type:

    type In<V> = (v: V) => V;
    function invariance<U, T extends U>(t: T, u: U, inT: In<T>, inU: In<U>) {
      u = t; // okay
      t = u; // error!
    
      inU = inT; // error!
      inT = inU; // error!
    }
    

Bivariance means that F<T> varies both with and against T: F<T> is both covariant nor contravariant in T. In a sound type system, this essentially never happens for any non-trivial type function. You can demonstrate that only a constant type function like type F<T> = string is truly bivariant (quick sketch: T extends unknown is true for all T, so F<T> extends F<unknown> and F<unknown> extends T, and in a sound type system if A extends B and B extends A, then A is the same as B. So if F<T> = F<unknown> for all T, then F<T> is constant).

But Typescript does not have nor does it intend to have a fully sound type system. And there is one notable case where TypeScript treats a type function as bivariant:

  • Method types both co-vary and contra-vary with their parameter types (this also happens with all function types with --strictFunctionTypes disabled):

    type Bi<V> = { foo(v: V): void };
    function bivariance<U, T extends U>(t: T, u: U, biT: Bi<T>, biU: Bi<U>) {
      u = t; // okay
      t = u; // error!
    
      biU = biT; // okay
      biT = biU; // okay
    }
    

Playground link to code

like image 55
jcalz Avatar answered Oct 18 '22 04:10

jcalz