Mule or horse, take it out for a walk.

Uncaught TypeError: Cannot read property ‘foo’ of undefined or Uncaught TypeError: Foo. slice is not a function, and then the screen goes blank. With the advent of TS, there is finally someone who can control the types of JS variables during development and compilation.

In the last article we looked at the behavior of TS in the “type declaration space”. How does the product of the “type declaration space” constrain the “variable declaration space”? What information does the “variable declaration space” provide for the “type declaration space”? That’s what this piece is about — communication between two Spaces.

In this article you will see:

  • How does the type declaration space provide type annotations for declarations in the variable declaration space
  • In what scenarios will TS be “annotated automatically”? What are the rules of inference?
  • If the variable’s existing or inferred type is inaccurate, can we correct it? What could possibly go wrong?
  • How does TS automatically narrow the type range through JS logic statements?
  • How do we extract type declarations from the variable declaration space?

1 Type annotations

From the “type declaration space” to the “variable declaration space”, most fundamentally, we can provide type constraints for variables through type annotations. As shown in the figure below, any artifacts we constructed in the “type declaration space” in the previous article can be annotated directly.

What is constraint? For example, if I have a pet named Tony and I annotate that it is a dog, when you call tony.fly (), an error will be reported at compile time or even when you write the code, so you don’t have to throw the dog when you execute and realize that it doesn’t have the fly method.

The basic annotation

A colon is the most basic annotation that completes the mapping from the type declaration space to the variable declaration space.

// Directly annotate the primitive type
const foo: number = 1;
// Annotate the declared type
type bar = number;
const foo: bar = 1;
// Annotate with type expressions
const baz: number | string = 1;
// Annotate with interface expressions
const obj: {
	foo: number;
	bar: string;
} = { foo: 1.bar: 'string' };
Copy the code

Function annotation

Declare the type first and then annotate it

Last time we looked at two ways to declare function types:

// Declare the invocation mode
interface Foo {		// Type Foo =
  (bar: string) :number;
};
// Declare the function type
type Foo = (bar: string) = > number;
Copy the code

Function variables declared as literals can then be annotated directly, much in the style of the colon annotations.

const getLen: Foo = (input) = > input.length;
Copy the code

The problem with this is that you can’t see the input and output types directly in the function declaration, so the readability is just a little bit worse.

Comment directly to the function declaration

It is better to comment directly in the declaration. For example, the above literal declaration can also be directly commented:

const getLen = (input: string) :number= > input.length;
Copy the code

This way we can clearly see the input and output types in the function declaration line, especially if you don’t want to write the return types by hand (you want to rely on type inference), there is no need to write the parameter types elsewhere. The same applies to functional expressions:

function getLen(input: string) :number {
  return input.length;
}
Copy the code

Or, I can make some of the parameters optional

const getLen = (input: string, options? :any) :number= > input.length;
Copy the code

overloading

Another advantage of directly annotating function declarations is that it makes it easier to extend function overloading.

A lot of times, our function doesn’t just have one passing argument, like CSS padding, where I can give one, two, four, right? How do I declare that?

function padding(a: number, b? :number, c? :number, d? :number) :string= >{... };Copy the code

Here are a few questions:

  1. This declaration allows me to pass three parameters, so the type constraint is not strict enough;
  2. Because the meaning of parameters is different when the number of parameters is different, when someone calls my function, I can’t tell him clearly what each parameter means by parameter name. I can only use general abcd.

Examples of padding:

// Overload three declaration headers
function padding(all: number);
function padding(topAndBottom: number, leftAndRight: number);
function padding(top: number, right: number, bottom: number, left: number);
// The actual header used and not exposed as a type annotation
function padding(a: number, b? :number, c? :number, d? :number) {
  if (b === undefined && c === undefined && d === undefined) {
    b = c = d = a;
  } else if (c === undefined && d === undefined) {
    c = a;
    d = b;
  }
  return {
    top: a,
    right: b,
    bottom: c,
    left: d
  };
}
Copy the code

2 Type inference

In JS, variable creation is very frequent, the creation of a variety of ways, if I get a variable every time I have to annotate the way to give it the type, will not be tired? The good news is that TS takes this into account and, in many cases, will help you “guess” the type of the new variable based on the variable flow logic of your variable declaration space.

It sounds like I have a pet named Tony, and it has been noted that it is a dog, so Tony has a pup named Tim, and the system has reason to infer that Tim is a dog.

From right to left

What scenarios can be automatically inferred about such a good thing? Everything can be traced back to the type from assignment and variable flow. The so-called assignment and flow, without the equal sign, TS will try to infer the left type from the right.

Direct assignment

When a variable is assigned when it is created, TS can generalize its type from the assignment.

const foo = 1;	// Foo's type is inferred to be 1
Copy the code

So what if I just declare no assignment and no annotation? The variable will be inferred to be any, which is meaningless for a type system, and TS will tell you not to do that.

structured

This “ability to infer types from assignments” can be nested. If you assign a complex value to a variable, TS will nest down to a type based on the underlying type:

const bar = {		// The type of bar is inferred to be interface {baz: string; }
	baz: ' '};Copy the code

Arrays are also structured data:

const foo = [ 1.'2' ];	/ / foo is inferred for (string | number) []
Copy the code

You might wonder, why isn’t foo inferred to be a tuple of [number, string]? This involves a method of TS inference called the “best general type”.

To infer the type of Foo, we must consider the types of all elements. There are two options: number and String. The compute common type algorithm considers all candidate types and gives a type that is compatible with all candidate types.

Conversely, deconstruction can also infer types, using arrays as an example:

const foo = [ 1.2 ];
const bar = foo[0];	// bar is inferred to be number
Copy the code

operation

If the right-hand side of the equal sign is an operation, TS can also infer the type of the variable:

const foo = 1 + 1;	// foo is inferred to be number
const bar = 1 + '1';	// bar is inferred to be a string, and TS even knows the rules for casting
Copy the code

But there is more to the operation than that. The result of the operation on any given variable of type can be inferred:

let foo: number;
let bar: string;
const baz = foo + bar;	// baz is inferred to be string
Copy the code

function

In a sense, function assignment is also a structured definition. The “return value” part of a function type can be inferred as structuration.

const foo = () = > {	// foo is inferred to () => number
	return 1;
};
Copy the code

Similarly, the return value can be inferred from the internal variable type or parameter type.

const foo = (bar: number) = > {	// foo is inferred to () => number
	const baz = 2;
	return bar + baz;
};
Copy the code

Inside functions, however, things get more complicated, so there are special types for function return values, such as void and never.

const foo = () = > {	// foo does not return and is inferred as () => void
	doSomething();
};
const bar = () = > {	// if () => never
	throw new Error(a); };Copy the code

Error

In addition to determining the type of an unannotated variable, type inference is also used to detect unreasonable type assignments in a timely manner. Such as:

const foo: number = ' ';	// Error: Cannot assign type "string" to type "number"
Copy the code

From left to right

Conversely, if the left side of the equals sign has already identified the type, the assignment on the right side will also absorb the type on the left and attempt to constrain its own behavior. Because the values on the right are related to the strength of the context, this feature is called “categorizing by context.” For example, a function like this:

let foo: (bar: number) = > number; // The type of foo has been annotated
foo = (bar) = > {		// The function assignment here absorbs foo's specific type and parses it into parameters and return values, depending on the context
	return bar.length	// Error: attribute "length" does not exist on type "number"
};
Copy the code

Contextual categorization can be used in many situations. Usually contains function arguments, the right side of assignment expressions, type assertions, object member and array literals, and return value statements.

About wrapping objects

Another point is about wrapping objects. We know that there are several ways to create a string value in the variable declaration space. Different declaration methods infer different types by default: string or string.

var foo = new String("Avoid newing things where possible");	// String
var bar = "A string, in TypeScript of type 'string'";		// string
var baz = String('aaa');	// string
Copy the code

“String” is a primitive, but “String” is a wrapper object. If possible, string is preferred.

String refers to the interface defined in ES5.d. ts. You can imagine how this could be implemented:

// es5.d.ts
interface String {
	valueOf(): string;	
}
interface StringConstructor {
	new (value): String;
	(value): string;
}
declare const String: StringConstructor;
Copy the code

You can only assign strings to strings, not strings to strings.

foo = bar	/ / normal
bar = foo	// Cannot assign type "String" to type "String". "String" is a primitive, but "String" is a wrapper object. If possible, string is preferred.
Copy the code

3 Type Assertion

Tony the dog’s pup Tim was inferred to be a dog, but I knew Tim was not only a dog, but also a “Teddy” (a subtype of “dog”) and wanted to narrow down the type of tag further.

Yes, I know better than the compiler, I know that an entity has a more exact type than it already has. I needed a way to override the compiler’s assumptions. This is called “type assertion.” Both as and <> can be used to make assertions:

const foo: any;
(foo as string).length;
<string>foo.length;
Copy the code

Type assertions are commonly used to narrow down types,

Limits on assertions

Zhao Gao sent for a deer and said to The Second emperor with a smile on his face: “Your Majesty, I present you a fine horse.”

If left unchecked, assertions can be used to undermine the TS type environment. So assertions can only be used in some reasonable scenarios, specifically:

  • If type A is compatible with type B, then A and B can assert each other
  • Top-level types (any/unknown) can assert each other to any type
  • A union type can assert itself against any subset

Type compatibility in TypeScript is based on structural subtypes. A structural type is a way to describe a type using only its members. The basic rule of TypeScript’s structured type system is that if X is compatible with Y, then Y has at least the same properties as X.

Compatibility is compatible regardless of how the type is defined, as long as one party satisfies all the attributes defined by the other.

Let’s get back to the subject and look at the limits of assertions with the following examples:

let foo: number;				foo as any;						// ok, top-level type
let foo: any; 				foo as number;					// ok, top-level type
let foo: number | string; 	foo as number;					// ok, union type
interface Parent { p: string }
interface Child extends Parent { c: number }
let foo: Parent;				foo as Child;					/ / ok, compatible
let foo = [1.'a'];			foo as [number.number];		// ok, union type

let foo: number;				foo as string;					// No, no, no
interface Parent { p: string }
interface Child { c: number }
let foo: Parent;				foo as Child;					// Not ok, not compatible
Copy the code

Double assertion

Zhao Gao sent for a deer and said to The Second emperor with a smile on his face: “Your Majesty, I will present you a deer or a horse.” After a pause, he added, “Your Majesty, the animal is a horse.”

This is the risk of assertions. I first assert that the type is a loose intermediate type, and then assert that the type cannot be asserted directly, and still achieve the effect of “pointing the dog”.

let foo = 1;	foo as string;							// No, the conversion from type "number" to type "string" may be wrong. If this is intentional, please first convert the expression to "unknown ".
let foo = 1;	foo as number | string as string;		// Ok, union type becomes intermediate type
Copy the code

If this is intentional, please convert the expression to “unknown “. You can see that TS is fully aware of this risk, and even intentionally leaves out a “back door” like any, while the more common and convenient “intermediate types” in double assertions are any and unknown. However, the use of double assertions should be minimized.

4 Type Protection

With a smile on his face, Zhao Gao said to the Second emperor, “Your Majesty, I will present you a deer or a horse.” The second emperor touched the animal’s head: “It has no horns. It’s a horse. Ride it.”

Let’s examine what happened here. Emperor Qin first judged whether the value passed in the variable of the joint type “deer | horse” has the attribute of “Angle”, then the type was asserted to be “horse”, and finally called the method of “horse”, which makes sense.

const whatQinIIThink = (ani: Deer|Horse) = > {
	if(! ('Angle' in ani)) {
		const hor = (ani asHorse) hor.ride(); }}Copy the code

In fact, TS is smarter when! When (‘ horn ‘in ANI) is satisfied, the deer is automatically scratched out of ANI’s union type without manual assertion to narrow the range.

Emperor Ii: “No horns! Ride!” .

This feature is called type protection: in some JS statements, it is possible to protect the type in a smaller range of more precise types.

How type protection is triggered

Which JS statements can trigger type protection?

// 1. typeof
function doSome(x: number | string) {
  if (typeof x === 'string') {
    // In this block, TypeScript knows that 'x' must be of type 'string'
    console.log(x.substr(1)); // ok}}// 2. instanceof
class Foo { foo = 123 }
class Bar { bar = 123 }
function doStuff(arg: Foo | Bar) {
  if (arg instanceof Foo) {
    console.log(arg.foo); // ok
    console.log(arg.bar); // Error}}// 3. in
interface A {x: number}
interface B {y: string}
function doStuff(q: A | B) {
  if ('x' in q) {
    // q: A}}// 4
type Foo = {	kind: 'foo'; // literal type};
type Bar = {	kind: 'bar'; // literal type};
function doStuff(arg: Foo | Bar) {
  if (arg.kind === 'foo') {
    console.log(arg.foo); // ok
    console.log(arg.bar); // Error}}Copy the code

Custom type protection

But the above statement is only a simple judgment, sometimes more complicated.

Emperor Ii believed that horns alone could not distinguish a deer from a horse, so he had a logic in his mind: a deer with horns, a pattern and no mane.

When called, TS Type protection is triggered by simply declaring the return value of the judgment function to be of the form foo is Type.

function isDeer (ani: Deer|Horse) :ani is Deer {
	return ('Angle' in ani) && ('pattern' inani) && ! ('mane' in ani);
}
const whatQinIIThink = (ani: Deer|Horse) = > {
	if(! isDeer(ani)) { hor.ride();// ok}}Copy the code

summary

TS made a number of tricky but well-made decisions about annotating from the “type declaration space” to the “variable declaration space”. It balances the rigor of the type system with the flexibility of development, and balances the efficiency and accuracy brought by “automation”.

5 Type Capture

Finally, let’s look at how types flow backwards from the variable declaration space to the type declaration space. This behavior is called type capture.

Type capture is simple and is implemented through Typeof.

let foo = 123;
let bar: typeof foo; // The 'bar' type is the same as the 'foo' type (in this case: 'number')
bar = 456; // ok
bar = '789'; // Error: 'string' cannot be assigned to type 'number'
Copy the code

In some cases, type capture is also combined with the type operations mentioned in the previous article:

const colors = {
  red: '1'.blue: '2'
};
type Colors = keyof typeof colors;	// 'red' | 'blue'
Copy the code

summary

In this article we discuss the communication of types between two Spaces

  • Declaration products of the variable declaration space can be annotated as types of the type declaration space
  • Functions can be called flexibly and accurately by overloaded annotation of various parameter forms
  • TS will infer the type of declared variables based on the traceability of variable assignments, operations, and flows
  • In limited cases, developers can use type assertions to modify existing type annotations or inferences
  • TS automatically reduces the range of union types according to JS conditional statements for type protection
  • If you need to retrieve a variable’s type backwards, use Typeof to capture it