Covariance and inversion in typescript

Covariance and inversion in typescript

If we want to write better type programming, we must master the knowledge of CO variance and contra variance. This record is only for the convenience of quickly locating the problems in writing code according to this article when there are problems in the application of this knowledge in the future. This article is just a record of some learning materials. Many expressions in the article are personal understanding. After all, I have a low education background and poor English level. There are many incomprehensible expressions in the materials.

Source chain of data (need scientific Internet access)
Original link

First, we need to know the following concepts. In generic programming, generic parameters affect the relationship between types and generics.

Because ts adopts the structural type system, we use subtype to represent the allocable relationship. The covariance and inversion caused by generic parameters are designed by the designer for the problem of type security.

In the following examples, we take Dog and Animal as basic types and analyze them according to the type relationship Dog extends Animal

In the definition, list < T > is used to describe the relationship

definition

Co variance definition

In the case of Dog is subtype of Animal, list < dog > is subtypeof list < animal > we call this list < T > generic change covariant

Contra variance definition

In the case of Dog is subtype of Animal, list < animal > is subtypeof list < dog > we call this list < T > generic change as inverse

In variance definition (here we can understand it as an independent type)

In the case of Dog is subtype of Animal, list < animal > has nothing to do with list < dog >

Bi variance definition (can assign values to each other)

In the case of Dog is subtype of Animal, list < animal > and list < dog > can be assigned to each other

type List<T> = T[]
const animals:List<Animal> = [] as List<Animal>
const dogs:List<Dog> = animals      //no error
const animals2: List<Animal> = dogs //no error

After completing these definitions, we can look at several examples to understand the significance of doing so.

example

//base types
class Animal {
  public weight = 0;
}
class Dog extends Animal {
  public isGoodBoy = true;
  public bark() {
    console.log("wofi");
  }
}
class Cat extends Animal {
  public isPlotting: "yes" | "no" = "yes";
  public play() {
    console.error("miau");
  }
}

Example 1

interface Cage<T> {
  readonly animal: T;
}
let dogCage: Cage<Dog> = { animal: new Dog() };
let cage: Cage<Animal> = dogCage; //Here, it is type safe, because Animal is the highest level of the relationship chain, and all objects related to it are extended
let catCage: Cage<Cat> = cage; //Here, the cage has actually been replaced with dogCage by the previous operation, and the ts compiler has checked the error
catCage.animal.play(); 		   //Suppose we let the compilation pass, the dog object exists in catCage, and there is no play method, so the type is unsafe

Example 1 shows that the readonly field causes the co variance of the generic relationship

Example 2

interface Mother<T> {
  create: () => T;
}
let dogMother: Mother<Dog> = {
  create() {
    return new Dog();
  },
};
let animationMother: Mother<Animal> = dogMother;//success
let catMother: Mother<Cat> = animationMother; //error:'Mother<Animal>' is not assignable to type 'Mother<Cat>'
catMother.create().play(); //As in example 1, dogs are treated as cats. The type is unsafe = > return type is co variance

Example 2 shows that the return type of the function signature method causes the contra variance of the generic relationship

Example 3

//Example 3
interface Groomer<T> {
  cuthair: (animal: T) => void;
}
let dogGroomer: Groomer<Dog> = {
  cuthair(dog) {
    dog.bark(); //The operation unique to the subclass is called here
  },
};
let animalGroomer: Groomer<Animal> = dogGroomer; //Error: because the dog is an extension of animal, there is no bark method in animal
animalGroomer.cuthair(new Animal()); //Assuming that compilation can be performed here, an error will be reported when running dog.bark is not a function
let animalGroomer2: Groomer<Animal> = {
  cuthair(animal) {
    console.log(animal.weight); //All animals have weight, so cats can also cuthair
  },
};
let catGroomer: Groomer<Cat> = animalGroomer2; //When the change of method parameters causes contra variance

Example 3 shows that the parameter bit of the function signature method causes the 'contra variance' of the generic relationship

Draw a general conclusion

  • 1. The output realized by the method is co variance
  • 2. Cotra variance is involved in the method implementation

supplement

For function parameter bits, when I write the conclusion above, I have the words function signature

What is method signature?

interface lll {
	methods(): void;           	//method signature
	functions: () => void;		//function signature
}

In order to make these two representations different in type programming, we need -- strictFunctiontypes, which is aimed at checking the function parameter bits, and the function signature will adopt a more strict method

  1. strictFunctiontype only affects function signatures, so the method signature is a bi variance. For example, the method signature in lib.es5.ts library is similar to Array, and the method signature is not affected by strictFunctiontype
  2. Contra variance - when used in function signature s with strictfunctiontype
  3. bi-variance - when used in function signatures without strictFunctiontype
  4. bi-variance - when used in method signatures

Why is the variance at the parameter position so complex?

In order to migrate faster in the user group of js, for experienced players, you can use strictFunctiontype to write better type code, and for junior players, you can avoid these compilation problems.

Type programming recommendations

  1. You can use readonly more
  2. Use function signatures more
  3. Consider variance in the interface
    1. Preferred co / contra variance
    2. Second, use invariance
    3. Minimize Bi variance

Although Bi variance won't bring you many red wavy lines, the benefits of program maintainability brought by type programming are lower.

For example, change example 3

//Example 3
interface Groomer<T> {
  cuthair(animal: T): void; //Here, the original function signature is changed to method signature
}
let dogGroomer: Groomer<Dog> = {
  cuthair(dog) {
    dog.bark(); //The operation unique to the subclass is called here
  },
};
let animalGroomer: Groomer<Animal> = dogGroomer;
animalGroomer.cuthair(new Animal()); //There will be no type check error here, but dog.bark is not a function error will be reported at runtime

Tags: TypeScript ts

Posted on Tue, 21 Sep 2021 01:59:21 -0400 by viv