TypeScript (fourteen) variants (covariant and contravariant)

Table of contents

foreword

"duck type"

subtyping

definition

features

assignment compatibility

Reflexive

transitivity

covariant

inverter

double variable

constant

think

see an example

what is the reason?

return value

parameter

Summarize

related articles


foreword

This article is included in the series of TypeScript knowledge summary articles , corrections are welcome! 

The first time I came into contact with the concept of variant is to understand TypeScript in depth. The conversion between types is called variant or variant. In TS, whether types can assign values ​​to each other, whether an error will be reported, and whether it is safe or not are all related to variants. This article will take you through the variants in ts

In Java, each class is an individual. For example, we define two classes, Dog and Cat, which have the same structure.

// Dog.java
public class Dog {
    String color;
    int age;
}

// Cat.java
public class Cat {
    String color;
    int age;
}
// Main.java
public class Main {
    Dog myCat = new Cat();// cannot convert from Cat to Dog
    Cat myDog = new Dog();// cannot convert from Dog to Cat
    Cat myCat2 = new Cat();// 允许
    Dog myDog2 = new Dog();// 允许
}

At this time, use the control variable to instantiate the two. It can be seen that although Dog and Cat have the same attributes, both have color and age attributes, they cannot declare types and assign each other.

"duck type"

Duck typing is a programming concept of a dynamic type mechanism. It means that the type of an object is determined by what it can do, not what class it belongs to. It will judge the type of the object at runtime. The idea of ​​the duck type is: if it looks like a duck, swims like a duck, quacks like a duck, then it is a duck.

Type checking in TS is a duck type system, if two object types have the same properties or methods, then they are considered to be the same type

We achieve the above effect in TS

class Dog {
    color: string
    age: number
}
class Cat {
    color: string
    age: number
}

const dog: Cat = new Dog()// 允许
const cat: Dog = new Cat()// 允许

It can be seen that the modes of TypeScript and JAVA are different, as long as the structure is the same, types can be reused, which is different from other languages.

subtyping

When learning mathematical sets, one set A is a subset of another set B, then A can be regarded as a subtype of B; a square can be regarded as a subtype of rectangle, because it inherits the properties of rectangle, and adds some additional constraints on the basis of rectangle

definition

In computer science, we often say that the inheritance of a class is the extension of the parent class , and subtyping is the extension of the parent type , which means that one type (subtype) is a special form of another type (supertype). Like inheritance, it can add or override properties and methods of supertypes. It is a more abstract way of expansion. It does not expand specific values, but expands types. The type product of a supertype after subtyping is called a subtype.

features

assignment compatibility

If IDog is a subtype of IAnimal, then a variable of type IDog can be assigned to a variable of type IAnimal, for example

interface IAnimal {
    name: string
}
interface IDog extends IAnimal {
    color: string
}
const animal: IAnimal = {
    name: "阿黄"
}
const dog: IDog = Object.assign(animal, {
    color: "black"
})
const _animal: IAnimal = dog // 可以执行

Among them, IDog inherits from IAnimal. If these two types are implemented using variables, the variables of the subtype can be assigned to the variables of the parent type.

Reflexive

Any type is a subtype of itself.

interface IAnimal {
    name: string
}
const animal: IAnimal = {
    name: "阿黄"
}
const _animal: IAnimal = animal

transitivity

If IDog is a subtype of IAnimal and IWhiteDog is a subtype of IDog, then IWhiteDog is also a subtype of IAnimal

interface IAnimal {
    name: string
}
interface IDog extends IAnimal {
    color: string
}
interface IWhiteDog extends IDog {
    isWhite: boolean
}
const animal: IAnimal = {
    name: "阿黄"
}
const dog: IDog = Object.assign(animal, {
    color: "black"
})
const whiteDog: IWhiteDog = Object.assign(dog, {
    isWhite: true
})
const _animal: IAnimal = whiteDog // 可以执行

In addition to the above points, subtypes also have the characteristics of variable covariance and function parameter inversion, which we will expand on below.

covariant

If you understand the concepts of duck typing and subtyping above, covariance (Covariance) is not difficult to understand. It is a type of conversion , just like the example in subtyping, if the type IDog is a subtype of type IAnimal, then dog can be assigned to animal, this process is covariation, that is, more covariance and less change (subtype assignment to parent type)

In order to better understand the above example and subsequent cases, we write a tool type IsExtends, which is used to determine whether the two types are a type of inheritance relationship

type IsExtends<Son, Parent> = Son extends Parent ? true : false;
type animalExtendsDog = IsExtends<IAnimal, IDog>// false
type dogExtendsAnimal = IsExtends<IDog, IAnimal>// true

inverter

Contravariance is the opposite of covariance. If the assignment of the parent class to the subclass is established, it is called contravariance. Let's modify the above example into the following code, which is the process of inversion, that is, less inversion becomes more (the parent type is assigned to the subtype)

const _dog: IDog = animal // 无法执行,类型 "IAnimal" 中缺少属性 "color",但类型 "IDog" 中需要该属性。

However, under normal circumstances, the above code will throw an error, indicating that the property is missing. At this time, we can use type assertion to convert

const _dog: IDog = animal as IDog // 可以执行

But it is not safe to write like this. If the IDog attribute is missing in the animal, an error may be thrown

In addition, the parameters of the function are inverting (with double variable characteristics without type checking), we can use the function of TS to transform, first we write a tool type ToFun, and pass the type into the function as a parameter

type ToFun<P> = (params: P) => void

Then we pass the previous two types IAnimal and IDog into the function, and we will find that the result is completely opposite to the previous one.

type IsExtends<Son, Parent> = Son extends Parent ? true : false;
type ToFun<P> = (params: P) => void

type animalExtendsDogFn = IsExtends<ToFun<IAnimal>, ToFun<IDog>>// true
type dogExtendsAnimalFn = IsExtends<ToFun<IDog>, ToFun<IAnimal>>// false

tips : If both are true, you can turn on strictFunctionTypes or strict in tsconfig, and then the variant of the function parameter will be detected

At this point, we can convert the variable into the form of a function to achieve the effect of inversion

const animalFn: (animal: IAnimal) => void = (animal) => { }
const _dog: ToFun<IDog> = animalFn // 可以执行

double variable

Bivariance is called two-way covariance in many places. Personally, I think it is not accurate. Bivariance means that the type has both covariance and inversion properties. It may be more appropriate to call it covariance and inversion. We turn off strictFunctionTypes and strict in tsconfig, and the above example will display two true

type IsExtends<Son, Parent> = Son extends Parent ? true : false;
type ToFun<P> = (params: P) => void
type animalExtendsDogFn = IsExtends<ToFun<IAnimal>, ToFun<IDog>>// true
type dogExtendsAnimalFn = IsExtends<ToFun<IDog>, ToFun<IAnimal>>// true

Let's try using variables

const animalFn: (animal: IAnimal) => void = (animal) => { }
const dogFn: (dog: IDog) => void = (dog) => { }
const _dog: ToFun<IDog> = animalFn // 可以执行
const _animal: ToFun<IAnimal> = dogFn // 可以执行

constant

The concept of invariance is relatively simple. The two types will neither covariate nor invert. The two types are completely irrelevant.

interface IAnimal {
    name: string
}
interface IDog {
    color: string
}

let animal: IAnimal = {
    name: "阿黄"
}
let dog: IDog = {
    color: "black"
}

dog = animal// 不能执行,缺少对应属性
animal = dog// 不能执行,缺少对应属性

think

see an example

Thinking about this example , it should not be difficult to understand if you use the concepts of covariance and inversion mentioned above. Variable (return value) covariant, parameter contravariant

Let's implement the original example using TS

First create three new types, namely IAnimal, IDog, and IWhiteDog, which are inherited layer by layer from animals to white dogs

interface IAnimal {
    name: string
}
interface IDog extends IAnimal {
    type: string
}
interface IWhiteDog extends IDog {
    isWhite: boolean
}

Then we use the tool type to implement a function creation, and the tool for judging inheritance before

type Fun<P, R> = (params: P) => R// 创建以P为参数,R为返回值的函数
type IsExtends<Son, Parent> = Son extends Parent ? true : false;// 是否是继承关系

Finally, use the above two tools to detect the type: In addition to itself, when the parameter of the function is IAnimal and the return value is IWhiteDog type, this function is a subtype of IDogFn

type IDogFn = Fun<IDog, IDog> // IDog, IDog

type IAnimalWhiteDogFn = Fun<IAnimal, IWhiteDog>// IAnimal, IWhiteDog
type IAnimalFn = Fun<IAnimal, IAnimal>// IAnimal, IAnimal
type IWhiteDogFn = Fun<IWhiteDog, IWhiteDog>// IWhiteDog, IWhiteDog
type IWhiteDogAnimalFn = Fun<IWhiteDog, IAnimal>// IWhiteDog, IAnimal

type IsExtendsAnimalWhiteDog = IsExtends<IAnimalWhiteDogFn, IDogFn>// true
type IsExtendsAnimal = IsExtends<IAnimalFn, IDogFn>// false
type IsExtendsWhiteDog = IsExtends<IWhiteDogFn, IDogFn>// false
type IsExtendsWhiteDogAnimal = IsExtends<IWhiteDogAnimalFn, IDogFn>// false

The return value of the function is covariant like the variable, so the subclass follows the conventional inheritance and takes IWhiteDog; the parameter is contravariant, so contrary to the normal inheritance behavior, take IAnimal

what is the reason?

return value

It is not difficult to understand that the return value is covariant. When the function is executed, the result is the RHS (Right Hand Side) right operand, that is, the return value of the function is also assigned to the variable. We still use the above example of assignment compatibility to make a little modification and add a function to the outer layer

interface IAnimal {
    name: string
}
interface IDog extends IAnimal {
    color: string
}
const animal = () => ({
    name: "阿黄"
})
const dog = () => Object.assign(animal, {
    color: "black"
})
const _animal: typeof animal = dog // 可以执行

parameter

It is not safe for passed-in arguments to access properties in subtypes. Let's take an example of the subtype of the parameter. First, we add an IBlackDog type and implement IDogFn and other variables.

interface IAnimal {
    name: string
}
interface IDog extends IAnimal {
    type: string
}
interface IWhiteDog extends IDog {
    isWhite: boolean
}
interface IBlackDog extends IDog {
    isBlack: boolean
}

type Fun<P, R> = (params: P) => R// 函数
type IDogFn = Fun<IDog, IDog> // dog函数
const animal: IAnimal = {
    name: "阿黄"
}
const dog: IDog = Object.assign(animal, {
    type: "dog"
})
const whiteDog: IWhiteDog = Object.assign(dog, {
    isWhite: true
})
const blackDog: IBlackDog = Object.assign(dog, {
    isBlack: true
})

Then create a function parameter to receive a function, the function structure is Fun<IDog, IDog>. At this point, it can be deduced from the following code why covariant parameters are unsafe, and it may be a bit confusing

const example = (_fn: IDogFn): void => {
    _fn(blackDog)// _fn参数限制了IDog类型,所以实参可以传递blackDog,whiteDog,dog。这里我们传入blackDog
}
example(whiteDogFn)// 抛错,参数“_whiteDog”和“params” 的类型不兼容。

const whiteDogFn = (_whiteDog: IWhiteDog) => {
    _whiteDog.isWhite = false// 形参取IWhiteDog,但是实参传了blackDog,此时就会抛错,找不到isWhite,因为blackDog只有isBlack。所以使用形参使用IWhiteDog是不安全的,必须传递IDog类型,或者IAnimal,因为IAnimal有的属性,IDog都有
    return dog
}
const animalFn = (_animal: IAnimal) => {
    return dog
}
example(animalFn)// 允许执行

To explain the above code:

We define a function whiteDogFn, which receives a parameter IWhiteDog. At this time, we can directly call the property isWhite in IWhiteDog, and it is still normal here. But then we substitute this function into the example function, why does it throw an error? Because the IDogFn type restricts us to pass in the three types of blackDog, whiteDog, and dog, there will be problems if we pass in blackDog at this time, because blackDog does not have the isWhite attribute. How can we solve this problem? Control the parameter type of the function from the source, that is, limit the parameters of the whiteDogFn function to IAnimal in an inverse way. At this time, IAnimal only provides the property of name, and we can only call this property, and this property is IAnimal, IDog, IWhiteDog, and IBlackDog. It is safe to use this function program at this time

Summarize

The above is the entire content of the article. This article describes in detail the concept of variants in TS, and deeply explains the concepts of subtyping operations, covariance, inversion, double variation, and invariance. Among them, covariance is characterized by more changes and less, and inversion means less changes.

Thank you for reading. If you think the article is good, I hope to support the blogger!

related articles

Covariance and contravariance | In-depth understanding of TypeScript

How to understand the two-way covariance of ts function parameters? - SegmentFault 思否

Talk about TypeScript type compatibility, covariance, contravariance, two-way covariance and invariance - Nuggets

Covariance and Contravariance in TypeScript

Guess you like

Origin blog.csdn.net/time_____/article/details/130094264