Search code examples
typescriptcontravariancegeneric-variancecontravariant

Why the functions Contravariance with their paramaters in typescript?


I have recently studied the generic concepts in Typescript. I have a problem with understanding "Why do the functions contravariance with their parameters?". I know that:

  1. Covariance is ifT extends U (T is assignable to U), it is true that G<T> extends G<U> (G<T> is also assignable to G<U>)

  2. Contravariance is if T extends U, it's sure to conclude that G<U> extends G<T> (now G<U> is assignable to G<T>).

before asking the question in this forum, I read the following posts, However, I still have trouble understanding why Functions in Typescript are Contravariant in their parameters:

I see that not only functions are contravariant with their parameters in typescript, but some other languages are also true.

I feel so frustrated to accept it without finding out why it is true. Could you help me explain it or if i missing related knowledge, please tell me what it is.


Solution

  • I'll use the concrete example of Animal and Dog.

    class Animal {
      move(distanceInMeters: number = 0) {
        console.log(`Animal moved ${distanceInMeters}m.`);
      }
    }
     
    class Dog extends Animal {
      bark() {
        console.log("Woof! Woof!");
      }
    }
    

    There are two choices for allowing assignment between Dog and Animal, are we allowed to substitute Dog for Animal, and are we able to substitute Animal for Dog.

    const animal: Animal = new Dog; 
    animal.move(10);
    

    Because extends means that we have the members of the base, this is fine. Typescript allows this

    const dog: Dog = new Animal;
    dog.bark();
    

    There isn't a bark method in Animal, so this would fail at runtime. Typescript forbids this.

    Now let's look at function types.

    const dog: Dog = new Dog;
    const animal: Animal = new Animal;
    
    type UseAnimal: (animal: Animal) => void;
    type UseDog: (dog: dog) => void;
    
    const useAnimal: UseAnimal = (animal: Animal) => { animal.move(10); }
    const useDog: UseDog = (dog: Dog) => { dog.bark() }
    

    Language designers have the same choice about function parameters. Lets see what happens when we allow the two cases

    const dogFn: UseDog = useAnimal;
    dogFn(dog);
    

    Because we are passing a Dog to useAnimal it is fine, it can move(10) as above. Typescript allows this.

    const animalFn: UseAnimal = useDog;
    animalFn(animal)
    

    The Animal we pass to useDog would cause a runtime failure, because it lacks bark(). Typescript forbids this.