When you write TS, you often encounter complex types that you don’t understand very well, but that either give an error or just work. After the accumulation of some scenes, to learn the basic knowledge of more physical ~

1. Basic concepts

1.1 Types of ducks

When a bird is seen walking like a duck, swimming like a duck, and quacking like a duck, it can be called a duck.

In TS, an object is equivalent if its properties and methods are consistent.

interface A {
  run(): void;
}

interface NotA {
  run(): void;
}

function runA(a: A) {
  a.run();
}

var b: NotA = { run(){}}; runA(b);// No error, can run
Copy the code

1.2 Liskov Substitution Principle (LSP)

All references to base classes (superclasses) must be able to transparently use objects from their subclasses.

This principle is very important and runs through almost every type of judgment in TS. The definition of a subclass (subtype) and a superclass (parent type) is involved here. The official definition is more abstract, in layman’s terms: attributes/methods are more subtypes, and the subtypes are more specific than the parent types.

Why not just say attributes are more subtypes? Because there is joint type, 1 1 | 2 | 3 | 2 is a subtype, because the former is more “specific”. Union types are more like the concept of sets, where there are fewer attributes, but subsets.

1.3 Covariant, contravariant and constant

First, there is the symbol “โ‰ผ”, A โ‰ผ B means that A is A subtype of B.

๐‘“(โ‹…) is covariant. When ๐ดโ‰ค๐ต, ๐‘“(๐ด)โ‰ค ๐ด (๐‘“) is valid.

๐‘“(โ‹…) is invert. When ๐ดโ‰ค๐ต, ๐‘“(๐ด) โ‰ค๐ต is valid.

๐‘“ (โ‹…) is constant, when ๐ด ๐ต or less when the above two formulas are established, namely ๐‘“ (๐ด) and ๐‘“ (๐ต) no inheritance relationships between each other.

Of course, saw above these definition affirmation or a little understanding, really ยท confuse oneself, do meng others. This will be illustrated in the actual example below.

Covariant and contravariant in TS

There is an example to explain covariant and contravariant, but it is too complicated, mixing parameter types and value types, to make me explode in place for the first time, for the new person, it is better to explain separately

2.1 covariance

interface Animal {
  name: number;
}
interface Dog extends Animal {
  bark(): void;
}

/ / Dog โ‰ผ Animal
var animal: Animal;
var dog: Dog;
animal = dog; // Yes, according to LSP, the parent type can be replaced by the quilt type

// Covariant, slightly more complicated
var animals: Animal[];
var dogs: Dog[];
animals = dogs; / / set up

// Make it more complicated
var getAnimal = (): Animal= > {
  return animal;
};
var getDog = (): Dog= > {
  return dog;
};
getAnimal = getDog; / / set up
Copy the code

It is covariant in the type of value and return value, and the subtype and parent type have the same ownership relationship.

2.2 inverter

/ / Dog โ‰ผ Animal

var feedAnimal = (o: Animal) = > {};
var feedDog = (o: Dog) = > {
  o.bark();
};
feedDog = feedAnimal; // Founded, feedAnimal โ‰ผ feedDog
feedAnimal = feedDog; Bark () = bark(); bark() = bark()

// The following scenario exists
function func(f: typeof feedDog) {
  var d: Dog;
  f(d);
}
func(feedAnimal);
Copy the code

In the parameter type of a function, is contravariant, the relation of function and parameter is opposite. However, in TS, parameter types are bidirectional covariant (see section 3.1 below). If strictFunctionType is enabled in the project, strictFunctionType will be enabled, and only then will strictFunctionType be inverted.

If you understand the inverse, the TS FAQ in the why-are-functions-with-reduces-parameters-assignable – to-functions-that-more-parameters, can also be more clearly understood.

function handler(arg: string) {
    / /...
}

function doSomething(callback: (arg1: string, arg2: number) = >void) {
    callback('hello'.42);
}

// Expected error because 'doSomething' wants a callback of
// 2 parameters, but 'handler' only accepts 1
doSomething(handler);
Copy the code

This works because callback requires two parameters, which are more specific. Arg is the parent of arg1 + arg2.

  • Parameters<typeof callback> โ‰ผ Parameters<typeof handler>
  • According to the inverse, the handler function is a subtype of the callback function, handler โ‰ผ callback
  • According to the Richter substitution, the handler can replace the usage scenarios of the callback

3 in doubt

3.1 Why are the function parameter types of TS bidirectional?

The why-are-function-parameters-bivariant document explains why. I don’t really understand it, but it follows the documentation, which shows a mutable array scenario:

  • Given, Dog โ‰ผ Animal, then Dog[] โ‰ผ Animal[]?
  • Dog[] โ‰ผ Animal[]
  • Since Dog[] โ‰ผ Animal[], all properties/methods should satisfy this relationship, i.e. Dog[].push โ‰ผ Animal[].push
  • Dog[]. Push (x: Dog) => number (x: Animal) => number
  • (x: Animal) => number โ‰ผ (x: Dog) => number
  • Obviously, the two are contradictory

So, in order to get around this contradictory scenario, TS makes a compromise and allows the parameter type of the function to be bidirectional covariant. In contra 2.2 above, if “strict”: false, feedDog cannot be assigned to feedAnimal.

However, in fact, I have been looking at this example for a long time and cannot understand the reason. I feel that there is no strong inevitability.

In fact, I feel the official example is not good enough, because the mutable array example is so bizarre that I find it hard to justify this “compromise” because, even in strict mode, it works both ways:

var animals: Animal[] = [];
var dogs: Dog[] = [];
var feedAnimals = as.push;
var feedDogs = dogs.push;

feedDogs = feedAnimals; / / set up
feedAnimals = feedDogs; // Still true
Copy the code

So, why are mutable arrays so weird? Because even if we understand it strictly as we did before:

  • Given that Dog โ‰ผ Animal
  • (x: Animal) => number โ‰ผ (x: Dog) => number
  • Then Dog[].push(Cat) is legal

Obviously, in a real business scenario, this result is not safe.

Therefore, for such array scenarios, the relationship is safest only if it is constant, but since JS itself does not prohibit mutable array methods, we have to continue to compromise.

3.2 A suspicious phenomenon

function func(o: { a: string }) {}
var o = { a: 'a'.b: 1 };

func({ a: 'a'.b: 1 }); Argument of type '{a: string; b: number; }' is not assignable to parameter of type '{ a: string; }'.Object literal may only specify known properties, and 'b' does not exist in type '{ a: string; } '.

func(o); // It works here
Copy the code

The phenomenon is that reefer substitutions are only triggered by variable assignments, right?

Unfortunately, after a brief check, the COMPILER of TS does not have debugging information, so we cannot confirm whether this is the reason. There is a hint of the relevant code here: checker.ts. But, hell, this file, Github can’t preview directly (it’s too big), I think it’s better to forget, not so hardcore, almost ok…

The resources

  • Covariant and contravariant
  • why-are-function-parameters-bivariant