Covariant and contravariant are very important topics in programming theory. Used to express the compatibility (or inheritance) of the parent and child classes after safe type conversions. Is defined as: if A, B represents two types; F () stands for type conversion; A -> B means that A is A subclass of B.

  • whenf()Is covariant: ifA -> B,f(A) -> f(B)
  • whenf()Contravariant: IfA -> B,f(B) -> f(A)
  • whenf()If it is dual change: ifA -> B, then all the above are true
  • whenf()Is constant: ifA -> B, the above are not valid, there is no compatible relationship
class Animal {
  move(){
    console.log("animal is moving"); }}class Cat extends Animal {
  purr() {
    console.log("cat is purring"); }}class WhiteCat extends Cat {
  showoffColor() {
    console.log("see my hair color"); }}Copy the code

We call it the parent of Animial, Cat is a subclass of Animal. WhiteCat is a subclass of Cat, WhiteCat -> Cat -> Animal. According to the principle that the parent class is compatible with the subclass, we can know:

let animal: Animal;
let cat: Cat;
let whiteCat: WhiteCat;

animal = cat;
animal = whiteCat;
cat = whiteCat;
Copy the code

Throw out problem

Suppose we now have a function of type (param: Cat) => Cat. So what are the compatible types?

We can break this down into two parts parameter compatibility and return value compatibility.

  • (param: Cat) => voidWhat are the compatible types of?
  • () => CatWhat are the compatible types of?

Parameter compatibility

We assume (param: Cat) => void is A, then we have two functions:

  • B: (param: WhiteCat) => void
  • C:(param: Animal) => void

So which function is compatible with A?

Assuming compatibility with B

Then A = B holds:

let A: (param: Cat) = > void;
const B = (param: WhiteCat) = > {
  param.move();
  param.purr();
  param.showoffColor();
};

A = B;
A(new Cat());
Copy the code

The function runs to param.showoffcolor () with an error. So the hypothesis doesn’t work.

Assuming compatibility with C

Then A = C holds:

let A: (param: Cat) = > void;
const C = (param: Animal) = > {
  param.move();
};

A = C;
A(new Cat());
Copy the code

The function now runs successfully. So the hypothesis is true.

So (param: Animal) => void -> (param: Cat) => void. From the previous definition, we can see that the function parameters are contravariant.

Return value compatibility

We assume that () => Cat is A, where there are the following two functions:

  • B: () => Animal
  • C:() => WhiteCat

So which function is compatible with A?

Assuming compatibility with B

Then A = B holds:

let A: (a)= > Cat;
const B = (a)= > new Animal();

A = B;
const result = A();
result.move();
result.purr();
Copy the code

An error is reported when the function runs to result.purr(). So the hypothesis doesn’t work.

Assuming compatibility with C

Then A = C holds:

let A: (a)= > Cat;
const C = (a)= > new WhiteCat();

A = C;
const result = A();
result.move();
result.purr();
Copy the code

The function now runs successfully. So the hypothesis is true.

So () => WhiteCat -> () => Cat. From the previous definition, you can see that the return value of a function is covariant.

The reality of function parameter types

In TS, parameter types are bivariant, that is, both covariant and contravariant. Of course it’s not safe. This can be fixed by enabling strictFunctionTypes to ensure that the parameter types are contrariant.

So why does TS allow function argument types to retain dual conversions? Here is a very common example:

interface Event { timestamp: number; } interface MouseEvent extends Event { x: number; y: number } function listenEvent(eventType: EventType, handler: (n: Event) => void) { /* ... */ / listenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x, e.y)); // listenEvent(EventType.mouse, (e: Event) => console.log((e as MouseEvent).x, (e as MouseEvent).y))); // listenEvent(EventType.mouse, (e: Event) => console.log((e as MouseEvent).x, (e as MouseEvent).y)); listenEvent(EventType.Mouse, ((e: MouseEvent) => console.log(e.x, e.y)) as (e: Event) => void);Copy the code

If the function argument type is double-variable, the first form of the above code will compile without the need for the latter two detour methods.