preface

There are many concepts in TypeScript about the type system, and if you don’t know one or the other, you can get caught up in errors.

This article is about the concept of covariant and contravariant in type systems, and how covariant and contravariant occur and operate.

Type of relationship

All you need to understand something new is a good and complete context, so you need to understand the most basic type relationships first.

Types in TypeScript are all about values; duck types.

Father and son type

Common type

Assume that the following interface types exist:

interface Animal {
  age: number
}

interface Dog extends Animal {
  bark(): void
}
Copy the code

Dog is descended from Animal, that is to say, Dog is a subtype of Animal. We can call it Dog ≼ Animal.

As you can see, subclasses are more specific and have more properties or behaviors than their parent classes.

You can also see a phenomenon (also known as type compatibility) that arises because of the duck type.

Let animal: animal let dog: dog animal = dog // √ Because animal only needs age and dog has age and bark(), it is ok to assign to animal. Dog = animal // × Error: Property 'bark' is missing in type 'animal' but required in type 'dog '.Copy the code

Assignment fails with an error because animal lacks the bark() attribute required by dog.

Conclusion:

  1. Child types are more specific than parent types, parent types are more general relative to child types, and child types are more precise relative to parent types.
  2. Subtypes can always be assigned to parent types.

The joint type

Suppose there are the following types:

type Parent = 'a' | 'b' | 'c'
type Son = 'a' | 'b'

let parent: Parent
let son: Son

son = parent 
// × Error: Type 'Parent' is not assignable to type 'Son'.
// Type '"c"' is not assignable to type 'Son'.
parent = son 
/ /)
Copy the code

Parent may be ‘c’, but Son does not include the literal type ‘c’, so assignment fails with an error.

We can see from this case that Son ≼ Parent. Because Parent is broader, Son is more specific.

Think of it this way: A union type is equivalent to a set, and Son is a Prent subset. However, Son is a child of Parent.

Covariant and contravariant

Wikipedia definition

So let’s say we still have Animal and Dog.

Covariance

The covariant case is very simple and it’s just type compatibility, so covariant is everywhere.

let animals: Animal[]
let dogs: Dog[]

animals = dogs
Copy the code

Absolutely, for reasons I’ve said before and won’t repeat. That’s covariant.

Contravariance

The inverse phenomenon occurs only on function parameters in function types. Assume the following code:

let haveAnimal = (animal: Animal) = > {
  animal.age
}
let haveDog = (dog: Dog) = > {
  dog.age
  dog.bark()
}

haveAnimal = haveDog 
// Error: Type '(dog: Dog) => void' is not assignable to type '(animal: Animal) => void'.
// Types of parameters 'dog' and 'animal' are incompatible.
// Property 'bark' is missing in type 'Animal' but required in type 'Dog'.

haveAnimal({
  age: 123,})Copy the code

The incoming Animal does not have the bark() attribute required by haveAnimal, so it is incorrectly checked.

Note: The function arguments before TS are bidirectional covariant, that is, both covariant and contravariant, and this code does not report errors. But in today’s Version (Version 4.1.2) strictFunctionTypes are configured in tsconfig.json to fix this problem. (Enabled by default)

Then change the code to:

- haveAnimal = haveDog
+ haveDog = haveAnimal
Copy the code

Found completely no problem!

Since we pass in a subclass of Animal Dog when we run haveDog (actually run haveAnimal), as we said before, the subtype has more attributes than the parent type, so haveDog needs to access the attributes in Animal. There must be more in the Dog type.

It can be found that for two parent-child types as function parameters to build two function types, the parent-child relationship of the two function types is reversed, which is the inverse.

At the same time, the return value type is covariant as usual. (If you’re interested, try it yourself.)

Conclusion: In function types, parameter types are contravariant and return value types are covariant.

practice

There is the following code:

type NoOrStr = number | string
type No = number
let noOrStr = (a: NoOrStr) => {}
let no = (a: No) => {}
Copy the code

NoOrStr = no or no = noOrStr.

You can think about who’s the parent and who’s the subclass and then invert it.

Practice the answer

NoOrStr = no causes an error.

Resolution:

  • In practice, it can be seen asNo ≼ NoOrStr, for inverse conversion:NoOrStr ≼ no. A subclass can be assigned to a parent class, and a parent class cannot be assigned to a subclass, sono = noOrStrThat’s right, no problem,noOrStr = noAn error will be reported.
  • Or another way of looking at it,noOrStrCan deal withnumber | stringType, andnoCan only handlenumberType.
    • So whenno = noOrStrNo problem, because callno()Is only passed innumberType, andnoOrStrCan be handled includingnumberTwo types of values.
    • And when thenoOrStr = noIt’s a problem because of the callnoOrStr()When the incomingnumber | stringType, andnoCan only handlenumberType value when callednoOrStr()The incomingstringThe value of thenoCould not handle, so an error was reported.

conclusion

The sentiment of this article was born after I queried and understood a problem I met on the way to learn TS. Please point out any mistakes or omissions 🙂

Infer also extends: Infer can be used in different ways: covariant and inverse. See the documentation (I forget where it is).