• Note: There are currently no Chinese translations of the latest TypeScript official documentation available online, so there is a translation plan. Since I’m also a beginner to TypeScript, I can’t guarantee 100% accuracy in TypeScript translations, so I feel free to point out any errors in the comments section.
  • Translation content: The temporary translation content is TypeScript Handbook, and other parts of the translation document will be supplemented later.
  • Project address: typescript-doc-zh, if it helps you, you can click a star ~

The official document address of this chapter is

Type of contract

Suppose we now have a function called padLeft:

function padLeft(padding: number | string, input: string) :string {
    trjow new Error('Not implemented yet! ')}Copy the code

If the padding is number, it will be used as the number of Spaces in the input prefix; if it is string, it will be used directly as the input prefix. Now let’s try to implement some of the logic, assuming we pass the padding argument of type number to padLeft.

function padLeft(padding: number | string, input: string) {
  return "".repeat(padding) + input;
// Argument of type 'string | number' is not assignable to parameter of type 'number'.
// Type 'string' is not assignable to type 'number'.
}
Copy the code

Ah, there was an error passing the padding argument. TypeScript warned us, add number to number | string may get expected results, so it does, in fact. In other words, we don’t explicitly check whether the padding is a number in the first place, and we don’t handle the case that it’s a string. So let’s improve the code.

function padLeft(padding: number | string, input: string) {
  if (typeof padding === "number") {
    return "".repeat(padding) + input;
  }
  return padding + input;
}
Copy the code

If you think this looks like boring JavaScript code, you’re on to something. The TypeScript code does look a lot like JavaScript, except for the type annotations we add. The point here is that TypeScript’s type system is designed to make it as easy as possible for developers to write regular JavaScript code without having to struggle to achieve type safety.

While it may not seem like much, the process actually holds a lot of secrets. Just as TypeScript uses static typing to analyze values at runtime, it overlays type analysis on JavaScript runtime control flow structures like if/else, and includes triadic expressions, loops, truth checking, and so on, all of which have an impact on types.

TypeScript finds typeof padding === “number” in the if condition checking statement and treats it as a special code construct called “type protection.” TypeScript follows the execution path our program is likely to take and analyzes the most specific type a value can take at a given location. It looks at these special checking statements (known as “type protection”) and assignment statements, and refines the declared type into a more specific type, which is known as “type narrowing.” We can observe these types of changes in many editors.

function padLeft(padding: number | string, input: string) {
  if (typeof padding === "number") {
    return "".repeat(padding) + input; ^ ^ ^ ^ ^// (parameter) padding: number
  }
  returnpadding + input; ^ ^ ^ ^ ^ ^ ^// (parameter) padding: string
}
Copy the code

TypeScript understands several different constructs for shrinking types.

typeofType of protection

As we’ve seen, JavaScript supports typeof operators that give basic information about the types of runtime values. Similarly, TypeScript expects the operator to return an identified string like this:

  • "string"
  • "number"
  • "bigint"
  • "boolean"
  • "symbol"
  • "undefined"
  • "object"
  • "function"

As we saw in padLeft, this operator is often present in a large number of JavaScript libraries, and TypeScript understands this operator to shrink types in different branches.

In TypeScript, checking the return value of typeof is a form of type protection. Because TypeScript can code how Typeof operates on different values, it also knows some of the quirks of this operator in JavaScript. For example, notice that in the list above, Typeof does not return the string “null”. Look at the following example:

function printAll(strs: string | string[] | null) {
  if (typeof strs === "object") {
    for (const s of strs) {
                    ^^^^	
		// Object is possibly 'null'.
      console.log(s); }}else if (typeof strs === "string") {
    console.log(strs);
  } else {
    // do nothing}}Copy the code

In the printAll function, we try to check whether STRS is an object and thus whether it is an array type. (Arrays are also object types in JavaScript.) But in JavaScript, typeof NULL actually returns “object”! This is one of those legacy bugs.

Experienced developers may not come as a surprise, but not everyone has encountered this problem in JavaScript. Fortunately, let us know STRS TypeScript just contraction to string [] | null type rather than a string [] type.

This might be a good introduction to “truth value” checking.

The true value shrinkage

“Truthiness” may not be found in dictionaries, but you’ve probably heard this thing in JavaScript.

In JavaScript, we can use conditional statements in arbitrary expressions, for example, &&, | |, if statement, Boolean invert (!) And so on. For example, the if statement does not require that its conditions be of Boolean type.

function getUsersOnlineMessage(numUsersOnline: number) {
  if (numUsersOnline) {
    return `There are ${numUsersOnline}online now! `;
  }
  return "Nobody's here. :(";
}
Copy the code

In JavaScript, constructs like if first “force” the condition to a Boolean value to ensure that the accepted arguments are reasonable, and then choose the appropriate branch based on whether the result is true or false.

Values like the following are converted to false:

  • 0
  • NaN
  • ""(Empty string)
  • 0n(0bigintVersion)
  • null
  • undefined

All other values will be converted to true. You can always convert a value to a Boolean type by calling a Boolean function, or even shorter!! . (The latter has the advantage that TypeScript can infer this as a more specific literal Boolean type true, whereas the former can only infer as Boolean.)

// The following results are true
Boolean("hello"); // type: boolean, value: true!!!!!"world"; // type: true, value: true
Copy the code

This feature is often used in coding, especially to prevent values like NULL or undefined. For example, we tried using the printAll function:

function printAll(strs: string | string[] | null) {
  if (strs && typeof strs === "object") {
    for (const s of strs) {
      console.log(s); }}else if (typeof strs === "string") {
    console.log(strs); }}Copy the code

As you can see, we managed to get rid of the previous error by checking if STRS is true. This at least prevents scary mistakes like the following:

TypeError: null is not iterable
Copy the code

Remember, however, that truth checking for primitive types is often error-prone. For example, we tried rewriting printAll like this:

function printAll(strs: string | string[] | null) {
  / /!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  // Don't write that!
  / /!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  if (strs) {
    if (typeof strs === "object") {
      for (const s of strs) {
        console.log(s); }}else if (typeof strs === "string") {
      console.log(strs); }}}Copy the code

We wrapped the whole function body in a truth check, but there was a potential problem: we might not be able to handle empty strings correctly anymore.

TypeScript doesn’t give error messages here, but it’s a concern if you’re not familiar with JavaScript. TypeScript can always help you catch bugs ahead of time, but if you choose not to do anything with a value, there’s only so much TypeScript can do without being overly restrictive. If you need to, use a linter to make sure you handle situations like this correctly.

One last thing to note about truth shrinkage is that booleans are inverting! Negative branches can be screened out:

function multiplyAll(
  values: number[] | undefined,
  factor: number
) :number[] | undefined {
  if(! values) {return values;
  } else {
    return values.map((x) = >x * factor); }}Copy the code

Equal contraction

TypeScript also uses switch statements and things like ===,! ==, == and! = such equality checks to shrink the type. Here’s an example:

function example(x: string | number, y: string | boolean) {
  if (x === y) {
    // We can now call any 'string' method on 'x' or 'y'.x.toUpperCase(); ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^//(method) String.toUpperCase(): stringy.toLowerCase(); ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^//(method) String.toLowerCase(): string
  } else {
    console.log(x); ^ ^//(parameter) x: string | number
    console.log(y); ^ ^//(parameter) y: string | boolean}}Copy the code

In the example above, when we check that x and y are equal, TypeScript knows that their types must also be equal. Since string is a common type of x and y, TypeScript knows that x and y must both be strings in the first logical branch.

Similarly, we can check for specific literal values (as opposed to variables). In the previous example of truth shrinkage, the printAll function we wrote was potentially wrong because it did not properly handle the empty string case. Another way to think about it is that we exclude NULL through a specific check so that TypeScript can still properly remove null from STRS types.

function printAll(strs: string | string[] | null) {
  if(strs ! = =null) {
    if (typeof strs === "object") {
      for (const s of strs) {
                      ^^^^
                     // (parameter) strs: string[]
        console.log(s); }}else if (typeof strs === "string") {
      console.log(strs); ^ ^ ^ ^// (parameter) strs: string}}}Copy the code

JavaScript loose equality checks == and! = can also shrink the type correctly. If you’re not familiar with it, when you check for a value == null, you’re not just checking to see if the value is actually null, you’re also checking to see if the value is potentially undefined. The same goes for == undefined: it checks whether the value is null or undefined.

interface Container {
  value: number | null | undefined;
}
 
function multiplyValue(container: Container, factor: number) {
  // This check can remove both null and undefined
  if(container.value ! =null) {
    console.log(container.value); ^ ^ ^ ^ ^ ^// (property) Container.value: number
 
    // It is now safe to calculatecontainer.value *= factor; }}Copy the code

inOperator shrinkage

JavaScript’s in operator determines whether an object has a property. TypeScript sees this as a way to shrink potential types.

For example, suppose you have code “value” in x, where “value” is a string literal and x is a union type. A branch with a result of true will shrink X to a type with an optional attribute or a required attribute value, while a branch with a result of false will shrink X to a type with an optional attribute or missing attribute value.

type Fish = { swim: () = > void };
type Bird = { fly: () = > void };
 
function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    return animal.swim();
  }
 
  return animal.fly();
}
Copy the code

Again, optional attributes appear in both branches when contracted. For example, humans can both swim and fly (I mean by vehicles), so in the in check, this type would appear in both branches:

type Fish = { swim: () = > void };
type Bird = { fly: () = > void };
typeHuman = { swim? :() = > void; fly? :() = > void };
 
function move(animal: Fish | Bird | Human) {
  if ("swim" inanimal) { animal; ^ ^ ^ ^ ^ ^// (parameter) animal: Fish | Human
  } else{ animal; ^ ^ ^ ^ ^ ^// (parameter) animal: Bird | Human}}Copy the code

instanceofshrinkage

JavaScript has an operator that checks if a value is an instance of another value. More specifically, in JavaScript, x instanceof Foo checks if Foo. Prototype is included in x’s prototype chain. Although we won’t go into that here, and will talk more about it later when we talk about classes, it’s still useful for most values that can be constructed from new. As you might have guessed, Instanceof is also a form of type protection, and TypeScript can shrink types in branches protected by Instanceof.

function logValue(x: Date | string) {
  if (x instanceof Date) {
    console.log(x.toUTCString());
                ^
              // (parameter) x: Date
  } else {
    console.log(x.toUpperCase());
                ^
              // (parameter) x: string}}Copy the code

The assignment

As we mentioned earlier, when we assign a value to an arbitrary variable, TypeScript looks at the right side of the assignment statement and shrinks the left side of the variable type appropriately.

let x = Math.random() < 0.5 ? 10 : "hello world!";
 	^  
   // let x: string | number
x = 1;
 
console.log(x);
            ^     
          // let x: number
x = "goodbye!";
 
console.log(x);
            ^ 
          // let x: string
Copy the code

Note that these assignments are valid. Although the observable type of x changes to number after the first assignment, we can still assign a string value to it. This is because the statement type x – that is, the initial type of x, is string | number, and can be assigned a value will always be based on the declared type checks.

If we assign x a Boolean value, an error will be thrown because the Boolean type does not exist in the declared type.

let x = Math.random() < 0.5 ? 10 : "hello world!";
    ^
  // let x: string | number
x = 1;
 
console.log(x);
 		   ^	          
          // let x: number
x = true;
^
// Type 'boolean' is not assignable to type 'string | number'.
 
console.log(x);
            ^        
           // let x: string | number
Copy the code

Control flow analysis

So far, we’ve shown you some basic examples of how TypeScript contracts types in specific branches. But TypeScript does a lot more than just analyze every variable and look for type protection in conditional statements like if and while.

function padLeft(padding: number | string, input: string) {
  if (typeof padding === "number") {
    return "".repeat(padding) + input;
  }
  return padding + input;
}
Copy the code

PadLeft returns in the first if block. TypeScript can parse this code and find the rest of the function body (return padding + input;). It’s not reachable when the padding is number. Finally, in view of the rest of the function body, which number can be removed from the padding type, and type is string | contraction number to a string).

This reachability-based code analysis is called “control flow analysis.” TypeScript uses this flow analysis to shrink types when encountering type protection and assignment statements. When analyzing a variable, the control flow can be broken down and recombined, and we can observe that the variable has a different type at each node.

function example() {
  let x: string | number | boolean;
 
  x = Math.random() < 0.5;
 
  console.log(x);
              ^
             // let x: boolean
 
  if (Math.random() < 0.5) {
    x = "hello";
    console.log(x);
                ^
               // let x: string
  } else {
    x = 100;
    console.log(x);
                ^ 
              // let x: number
  }
 
  return x;
         ^
        // let x: string | number
}
Copy the code

Use type predicates

So far, we’ve used off-the-shelf JavaScript constructs to handle type shrinkage, but sometimes, you might want to control type changes more directly in your code.

To implement a user-defined type protection, we simply define a function that returns a type predicate:

function isFish(pet: Fish | Bird) :pet is Fish {
  return (pet asFish).swim ! = =undefined;
}
Copy the code

In this case, PET is Fish is a type predicate. The Type predicate is of the form paramenterName is Type, and parameterName must be the parameterName of the current function signature.

Any time you pass an argument to isFish and call it, TypeScript contracts the variable type to the specific type when the type is compatible with the initial type.

// Calls to swim and fly are ok
let pet = getSmallPet();
 
if (isFish(pet)) {
  pet.swim();
} else {
  pet.fly();
}
Copy the code

Note that TypeScript knows not only that pet is Fish in the if branch, but also that it corresponds to Bird in the else branch, since it must be Bird if it is not Fish.

You can also use this type of protection from a Fish | isFish Bird type of an array of scent in array contains only Fish type:

const zoo: (Fish | Bird)[] = [getSmallPet(), getSmallPet(), getSmallPet()];
const underWater1: Fish[] = zoo.filter(isFish);
// or use
const underWater2: Fish[] = zoo.filter(isFish) as Fish[];
 
// In more complex examples, repeating type predicates may be required
const underWater3: Fish[] = zoo.filter((pet): pet is Fish => {
  if (pet.name === "sharkey") return false;
  return isFish(pet);
});
Copy the code

In addition, classes can use this is Type to shrink types.

An identifiable type of union

Most of the examples we’ve seen so far have shrunk single variables to simple types such as String, Boolean, and number. While this is common, in JavaScript we often have to deal with slightly more complex structures.

Suppose we now need to code the shape of a circle and a square, the circle using the radius and the square using the side length. We will use the Kind field to indicate the shape we are currently processing. Here is the first way to define Shape:

interface Shape {
    kind: "circle" | "square"; radius? :number; sideLength? :number;
}
Copy the code

Note that we use a union of string literal types: “circle” and “square”. It can tell us whether the shape we are currently working with is a circle or a square. Through the use of “circle” | “square” rather than a string, we can avoid spelling errors.

function handleShape(shape: Shape) {
  // oops!
  if (shape.kind === "rect") {
/ / the conditions always return false, because of type "circle" | "square" and type "the rect" there is no overlap.
    // ...}}Copy the code

We can write a getArea function that uses logic based on the type of shape being processed. First let’s deal with circles:

function getArea(shape: Shape) {
  return Math.PI * shape.radius ** 2;
// The object may be 'undefined'
}
Copy the code

An error is thrown when strictNullChecks is enabled – which makes sense, since RADIUS may not be defined. But what if we do a reasonable check on the kind attribute?

function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius ** 2;
   // Object might be 'undefined'}}Copy the code

Emm, TypeScript is still nowhere to go. We happen to have a situation here where we have more information about this value than the type checker. Therefore, a non-null-value assertion (suffixes shape.radius!) can be used here. This indicates that radius must exist.

function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius! ** 2; }}Copy the code

But this approach seems less than ideal. We had to add a lot of non-null assertions to the type checker (!). Make sure shape.radius is defined, but these assertions can easily be false if the code is removed. Furthermore, with strictNullChecks disabled, we might accidentally access other fields (after all, TypeScript assumes optional attributes exist when it reads them). In short, there should be a better way.

The problem with Shape encoding is that the type checker is completely unable to determine the existence of RADIUS and sideLength based on the Kind attribute. We must convey what we know to the type checker. With that in mind, let’s define Shape again.

interface Circle {
  kind: "circle";
  radius: number;
}
 
interface Square {
  kind: "square";
  sideLength: number;
}
 
type Shape = Circle | Square;
Copy the code

Here, we appropriately divide Shape into two types that have different KIND property values, but radius and sideLength are required properties in their respective types.

Let’s look at what happens when a RADIUS tries to access Shape:

function getArea(shape: Shape) {
  return Math.PI * shape.radius ** 2; ^ ^ ^ ^ ^ ^// Property 'radius' does not exist on type 'Shape'.
  // Property 'radius' does not exist on type 'Square'.
}
Copy the code

Just like when we first defined Shape, we still threw an error. Previously, we saw an error when radius was an optional attribute (only with strictNullChecks enabled) because TypeScript had no way of knowing if the attribute really existed. Now that Shape is a federated type, TypeScript tells us that Shape could be Square, and Square doesn’t have a RADIUS attribute! Both explanations are reasonable, but only the latter will throw an error even if strictNullChecks is disabled.

So what happens if we check the kind property again at this point?

function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius ** 2; ^ ^ ^ ^ ^// (parameter) shape: Circle}}Copy the code

The code no longer reports errors! When each type in a union type contains the common property of a literal type, TypeScript treats it as a recognizable union type and validates a member of the union type by contracting.

In this case, kind is the public property (that is, an identifiable property of Shape). By checking if the kind attribute is “circle”, we can exclude all types in our Shape that do not have a kind attribute value of “circle”. That is, you can shrink shape to Circle.

Similarly, this check can be used in switch statements. Now we can write a complete getArea function, and it doesn’t have any trouble! Nonnull-value assertion symbol.

function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2; ^ ^ ^ ^ ^// (parameter) shape: Circle
    case "square":
      return shape.sideLength ** 2; ^ ^ ^ ^ ^// (parameter) shape: Square}}Copy the code

This example focuses on the encoding of Shape. It’s important to communicate important information to TypeScript, telling it that Circle and Square are two different types that have their own kind attribute values. This allows us to write type-safe TypeScript code that looks no different from the JavaScript we write. Knowing this, the type system can also do the “right” thing and figure out the specific type in each branch of the Switch.

By the way, you can try writing the example above and remove some of the return keywords. As you’ll see, type checking can help avoid bugs when you accidentally encounter different clauses in switch statements

Recognisable union types are very useful, and not just for circles and squares in this example. They are also useful for representing any type of messaging scheme in JavaScript, such as sending messages over a network (client/server communication) or mutation encoding in a state management framework.

nevertype

When you shrink a type, you can reduce the union type to a single remaining type, at which point you have basically eliminated all possibilities, and there are no more types left to choose from. In this case, TypeScript uses the never type to indicate a state that should not exist.

Exhaustive check

The never type can be assigned to any type, but no type can be assigned to never except never itself. This means that you can use type contraction and never to do exhaustive checking in a SWicTH block.

For example, in the default branch of getArea, we can assign shape to a value of type never. Thus, an error must be thrown in this branch if any of the possible cases were not handled in the previous branch.

type Shape = Circle | Square;
 
function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    default:
      const _exhaustiveCheck: never = shape;
      return_exhaustiveCheck; }}Copy the code

Adding a new member to the Shape union type causes TypeScript to throw an error:

interface Triangle {
  kind: "triangle";
  sideLength: number;
}
 
type Shape = Circle | Square | Triangle;
 
function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    default:
      const _exhaustiveCheck: never= shape; ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^//Type 'Triangle' is not assignable to type 'never'.
      return_exhaustiveCheck; }}Copy the code