Literal type, type inference, type broadening and type narrowing

4.1 introduced

In many cases, TypeScript will automatically infer the type of a variable based on the context, without requiring us to specify type annotations.

{
  let str: string = 'this is string';
  let num: number = 1;
  let bool: boolean = true;
}
{
  const str: string = 'this is string';
  const num: number = 1;
  const bool: boolean = true;
}
Copy the code

The above example can be simplified as follows:

{
  let str = 'this is string'; / / equivalent
  let num = 1; / / equivalent
  let bool = true; / / equivalent
}
{
  const str = 'this is string'; / / inequitable
  const num = 1; / / inequitable
  const bool = true; / / inequitable
}
Copy the code

Note: The word “simplify” is used here, not to say that the two examples are completely equivalent. We’ll go into that later.

We call TypeScript’s ability to infer types based on assignment expressions “type inference.”

4.2 Type Inference

In TypeScript, type annotation declarations are made after the variable (that is, after the type).

The advantage of using type annotations is that the compiler can deduce the corresponding type from the context of the code, instead of declaring the type of the variable, as shown in the following example:

{
  let x1 = 42; // Infer that the type of x1 is number
  let x2: number = x1; // ok
}
Copy the code

In TypeScript, variables with initialized values, function arguments with default values, and the types returned by functions can all be inferred from the context. For example, we can infer the return type from the return statement, as shown in the following code:

{
  /** The return value is of type number */
  function add1(a: number, b: number) {
    return a + b;
  }
  const x1= add1(1.1); // Infer that the type of X1 is also number

  /** Deduce that the type of the argument b is numeric or undefined, and the type of the return value is numeric */
  function add2(a: number, b = 1) {
    return a + b;
  }
  const x2 = add2(1);
  const x3 = add2(1.'1'); // ts(2345) Argument of type '"1"' is not assignable to parameter of type 'number | undefined
}
Copy the code

4.3 Context Inference

Using the example of type inference, we find that the type of a variable can be inferred from the value to which it is assigned. In addition, in some specific cases, we can infer the type of a variable from its context, as shown in the following example:

{
  type Adder = (a: number, b: number) = > number;
  const add: Adder = (a, b) = > {
    return a + b;
  }
  const x1 = add(1.1); // Infer that type X1 is number
  const x2 = add(1.'1');  // ts(2345) Argument of type '"1"' is not assignable to parameter of type 'number
}
Copy the code

Here we define a function of type Adder that implements the addition function, declare the add variable of type Adder, and assign an anonymous arrow function. The type and return type of the arrow function arguments A and B are not explicitly declared.

TypeScript uses add’s type Adder to infer the types of arrow function arguments and return values. And the return type is determined in the context of the variable declaration (more on function types in lecture 05).

TypeScript’s type inference mechanism and capabilities allow us to infer the type of a variable directly from the context without having to explicitly declare it.

Looking back at the previous example (shown below), we see that these variables annotated by default types can also be typed by type.

{
  let str = 'this is string'; // str: string
  let num = 1; // num: number
  let bool = true; // bool: boolean
}
{
  const str = 'this is string'; // str: 'this is string'
  const num = 1; // num: 1
  const bool = true; // bool: true
}
Copy the code

As the comments in the code above indicate, variables defined by let and const with the same value infer different types. For example, if ‘this is string’ is the same as ‘this is string’ (which represents a string value here), the variable defined by let is of type string, and the variable defined by const is of type ‘this is string’ (which represents a string literal here). We can test this conclusion here by looking at the types of variables in the VS Code Hover example.

4.4 Literal types

In TypeScript, literals can represent not only values but also types, known as literal types.

Currently, TypeScript supports three types of literals: string literals, numeric literals, and Boolean literals. The corresponding string, numeric, and Boolean literals have the same literal type as their values.

{
  let specifiedStr: 'this is string' = 'this is string';
  let specifiedNum: 1 = 1;
  let specifiedBoolean: true = true;
}
Copy the code

A literal type is a subtype of a collection type, which is a more concrete expression of a collection type. For example, ‘this is string’ (which represents a string literal) is of type string (or rather a subtype of string), String is not necessarily a ‘this is string’ type.

{
  let specifiedStr: 'this is string' = 'this is string';
  let str: string = 'any string';
  specifiedStr = str; // ts(2322) type '"string"' cannot be assigned to type 'this is string'
  str = specifiedStr; // ok 
}
Copy the code

4.5 String literal types

In general, we can use a string literal type as the type of a variable

let hello: 'hello' = 'hello';
hello = 'hi'; // ts(2322) Type '"hi"' is not assignable to type '"hello"'
Copy the code

In practice, it is not useful to define a single literal type; the real application is to combine multiple literal types into a union type (see section 08), which describes a practical set with explicit members.

As shown in the following code, we use the literal union type to describe an unambiguous, ‘up’ and ‘down’ set, so we know exactly what data structure is needed.

type Direction = 'up' | 'down';
function move(dir: Direction) {
  // ...
}
move('up'); // ok
move('right'); // ts(2345) Argument of type '"right"' is not assignable to parameter of type 'Direction'
Copy the code

By using the union type of the combination of literal types, we can restrict the parameters of a function to the specified set of literal types. The compiler then checks if the parameters are members of that set.

Therefore, using a literal type (the union type of a combination) can limit the parameters of a function to more specific types than using a string type. This not only improves the readability of the program, but also ensures that the parameter types of the function are guaranteed, thus killing two birds with one stone.

4.6 Numeric literal types and Boolean literal types

We can use the union type of the combination of literals to limit the parameters of a function to more specific types, such as declaring a type like Config:

interface Config {
    size: 'small' | 'big';
    isEnable:  true | false;
    margin: 0 | 2 | 4;
}
Copy the code

In the above code, we limit the size attribute as a string literal type ‘small’ | ‘big’ isEnable properties for the type of Boolean literals true | false (Boolean literals true and false, only The combination of true | false with direct use of Boolean no difference), margin properties for numeric literals type 0 | 2 | 4.

Let’s take a look at why variables defined by let and const have the same value but not the same type.

{
  const str = 'this is string'; // str: 'this is string'
  const num = 1; // num: 1
  const bool = true; // bool: true
}
Copy the code

In the above code, we define const as an immutable constant. In the case of default type annotations, TypeScript concludes that its type is directly determined by the type of the assigned literal, which is also a reasonable design.

Let’s look at the let example shown below, which may be a little harder to understand at this point.

{
  let str = 'this is string'; // str: string
  let num = 1; // num: number
  let bool = true; // bool: boolean
}
Copy the code

In the above code, the default explicit type-annotated variable type is converted to the parent of the assignment literal type. For example, STR is a string parent of the type ‘this is string’ (which represents a string literal here). The type of num is number, the parent type of type 1.

This design conforms to programming expectations, meaning that we can assign arbitrary values to STR and num, respectively (as long as variables of type are subsets of string and number) :

 str = 'any string';
  num = 2;
  bool = false;
Copy the code

We call this design of TypeScript literal quds for conversion to parent types “literal design “, which is the broadening of literal types, for example for the example of string literal for conversion to string, which we will focus on next.

4.7 Literal Widening

All variables, function parameters, and non-read-only attributes of objects defined by let or var that satisfy the conditions for specifying an initial value and not explicitly adding a type annotation are inferred to be the type of the specified initial value literal. This is called literal type broadening.

{
  let str = 'this is string'; // The type is string
  let strFun = (str = 'this is string') = > str; // The type is (STR? : string) => string;
  const specifiedStr = 'this is string'; // Type is 'this is string'
  let str2 = specifiedStr; // The type is 'string'
  let strFun2 = (str = specifiedStr) = > str; // The type is (STR? : string) => string;
}
Copy the code

For 2 ~ 3 lines meet the let, parameter and the condition of not explicitly declared type annotation, widened so variable, parameter type to string (parameter type is exactly in string | undefined).

Because the constants in line 4 are immutable and the type is not widened, specifiedStr is of the ‘this is string’ literal type.

In lines 5 and 6, the types of variables and parameters are broadened because the assigned value specifiedStr has a literal type and no explicit type annotations. In fact, such a design is in line with the actual programming demands. Let’s imagine that if the type of str2 is inferred to be ‘this is string’, it will not be changed because assigning a value to any other string type will prompt a type error.

Based on the condition of literal type broadening, we can control the type broadening behavior by adding the display type annotation as shown in the code below.

{
  const specifiedStr: 'this is string' = 'this is string'; // The type is '"this is string"'
  let str2 = specifiedStr; // Even if the let definition is used, the type is 'this is string'.
}
Copy the code

4.8 Type Widening

If a variable defined by let or var satisfies an unexplicitly declared type annotation and is given a null or undefined value, then the type of the variable is inferred to be any:

{
  let x = null; // Expand the type to any
  let y = undefined; // Expand the type to any
  /** ----- Dividing line ------- */
  const z = null; // The type is null
  /** ----- Dividing line ------- */
  let anyFun = (param = null) = > param; // The parameter type is null
  let z2 = z; // The type is null
  let x2 = x; // The type is null
  let y2 = y; // The type is undefined
}
Copy the code

In modern TypeScript, lines 2 and 3 of the above examples are type-broadening more in keeping with practical programming conventions. We can assign values of any other type to variables x and y that have null or undefined initial values.

With strictNullChecks=true turned on (note: all examples in this course are written in strict mode), think about the type-safe behavior in lines 7 to 10: Why are the variables and parameters in these lines null or undefined instead of any? The former allows us to be more careful with variables and parameters than the latter.

4.9 Type Narrowing

In TypeScript, you can narrow the Type of variables from a broad set to a relatively small and specific set through certain operations. This is called “Type Narrowing”.

{
  let func = (anything: any) = > {
    if (typeof anything === 'string') {
      return anything; // The type is string
    } else if (typeof anything === 'number') {
      return anything; // The type is number
    }
    return null;
  };
}
Copy the code

We can use type guards to narrow union types (see section 08) to specific subtypes, as shown in the following example:

{
  let func = (anything: string | number) = > {
    if (typeof anything === 'string') {
      return anything; // The type is string
    } else {
      return anything; // The type is number}}; }Copy the code

Of course, we can also converge the union type to a more specific type by using literal type equivalence (===) or other control flow statements (including but not limited to if, the ternary operator, and the switch branch), as shown in the following code:

{
  type Goods = 'pen' | 'pencil' |'ruler';
  const getPenCost = (item: 'pen') = > 2;
  const getPencilCost = (item: 'pencil') = > 4;
  const getRulerCost = (item: 'ruler') = > 6;
  const getCost = (item: Goods) = >  {
    if (item === 'pen') {
      return getPenCost(item); // item => 'pen'
    } else if (item === 'pencil') {
      return getPencilCost(item); // item => 'pencil'
    } else {
      return getRulerCost(item); // item => 'ruler'}}}Copy the code

In the getCost function above, the parameter types accepted are associative types of literal types. The function contains three process branches of the if statement, each of which calls functions whose parameters are specific and independent literal types.

Why can item, a variable of type consisting of multiple literals, pass values to functions getPenCost, getPencilCost, and getRulerCost that only receive a single particular literal type? This is because in each process branch, the compiler knows what type of item is in the process branch. For example, in the item === ‘pencil’ branch, the item type is narrowed to ‘pencil’.

In fact, if we remove the intermediate process branch from the above example, the compiler can also infer the convergent type, as shown in the following code:

const getCost = (item: Goods) = >  {
    if (item === 'pen') {
      item; // item => 'pen'
    } else {
      item; // => 'pencil' | 'ruler'}}Copy the code

5 Function type: return value type and parameter type

We can use functions to extract reusable logic, abstract models, and encapsulation processes. In TypeScript, there are classes, namespaces, and modules, but functions are also one of the most basic and important elements.

In TypeScript, we can define functions as function literals and arrow functions

function add() {}
const add = () = > {}
Copy the code

We can also explicitly specify the types of function arguments and return values

const add = (a: number, b: number): number= > {
     return a + b;
}
Copy the code

5.1 Return Value Type

In JavaScript, we know that a function can have no explicit return, in which case the return value of the function should be undefined:

function fn() {
  // TODO
}
console.log(fn()); // => undefined
Copy the code

It is important to note that in TypeScript, if we explicitly declare the return value of the function as of type UNdun, we will get an error notification like the one below.

function fn() :undefined { // ts(2355) A function whose declared type is neither 'void' nor 'any' must return a value
  // TODO
}
Copy the code

The correct way to do this is to use the void type to represent types in which the function does not return a value (this is the only scenario where the “loser” void type is useful).

function fn1() :void {
}
fn1().doSomething(); // ts(2339) Property 'doSomething' does not exist on type 'void'.
Copy the code

We can use syntax similar to defining an arrow function to represent the parameter and return value types of a function type. In this case, the => type is used to define a function type only, not to implement it.

Note that the => here is different from the => of the arrow function in ES6. => in TypeScript function types represents the definition of a function, with the type of its arguments on the left and the type of its return value on the right. In ES6 => is the implementation of the function.

type Adder = (a: number, b: number) = > number; // TypeScript function type definitions
const add: Adder = (a, b) = > a + b; // ES6 arrow function
Copy the code

Note here: the arrow function on the right does not explicitly declare a type annotation, but it can be inferred from the context type mentioned in Lecture 04.

In addition to using this declaration syntax in an object (that is, an interface type), we can also use shorthand syntax like object properties to declare properties of a function type, as shown in the following code:

interface Entity {
    add: (a: number, b: number) = > number;
    del(a: number.b: number) :number;
}
const entity: Entity = {
    add: (a, b) = > a + b,
    del(a, b) {
      returna - b; }};Copy the code

5.2 Default and inferred return value types

The type of the return value of a function can be inferred from TypeScript and is the default.

Function is a relatively independent context, we can process based on the value of the input parameters, and return a new value. At the type level, we can also use type inference (think type inference, context type inference in Lecture 04) to process the type of the input and return the new type, as shown in the following example:

function computeTypes(one: string, two: number) {
  const nums = [two];
  const strs = [one]
  return {
    nums,
    strs
  } // Return {nums: number[]; STRS: The type of string[]}
}
Copy the code

Please remember: This is an important and interesting feature, because type inference of function return values in combination with generics (which we’ll discuss in detail in Lecture 10) allows for extremely complex type calculations (which are inherently complex type inferences, and are called computations here to indicate complexity), For example, the association between the State, Reducer, and Effect types in the Redux Model.

Generally, the return type of a function in TypeScript is default and inferred, but there are special cases where we need to explicitly declare the return type, such as the return value of a Generator function.

5.3 Return values of Generator functions

The Generator function returns an Iterator object. We can use either the general-name interface of Generator or the general-name interface of Iterator to indicate the type of the returned value (the Generator inherits from Iterator), as shown in the following example:

type AnyType = boolean;
type AnyReturnType = string;
type AnyNextType = number;
function *gen() :Generator<AnyType.AnyReturnType.AnyNextType> {
  const nextValue = yield true; // The nextValue type must be number. Yield must be Boolean
  return `${nextValue}`; // The type must be string
}
Copy the code

5.4 Optional parameters and default parameters

function log(x? :string) {
  return x;
}
log(); // => undefined
log('hello world'); // => hello world
Copy the code

We add? Before the: of the type annotation. The x argument to the log function is default.

This does not mean that the default and type are undefined equivalent.

function log(x? :string) {
  console.log(x);
}
function log1(x: string | undefined) {
  console.log(x);
}
log();
log(undefined);
log1(); // ts(2554) Expected 1 arguments, but got 0
log1(undefined);
Copy the code

Here? : indicates that parameters can be default or not passed, that is, when calling a function, we can pass no parameters explicitly. However, if we declare the parameter type for XXX | undefined (using the combined type | here), it means the function is not the default parameters and type must be XXX or undfined.

While ES6 supports default arguments to functions, TypeScript inferences the types of function arguments based on the types of the default arguments to functions

function log(x = 'hello') {
    console.log(x);
}
log(); // => 'hello'
log('hi'); // => 'hi'
log(1); // ts(2345) Argument of type '1' is not assignable to parameter of type 'string | undefined'
Copy the code

According to the function of the default parameters’ hello ‘, TypeScript infer the x type string | is undefined.

Of course, TypeScript also explicitly declares the types of default arguments (this is usually only necessary if the default argument type is a subset of the argument type). At this point, however, the default parameter only acts as the parameter default

function log1(x: string = 'hello') {
    console.log(x);
}
// ts(2322) Type 'string' is not assignable to type 'number'
function log2(x: number = 'hello') {
    console.log(x);
}
log2();
log2(1);
log2('1'); // ts(2345) Argument of type '"1"' is not assignable to parameter of type 'number | undefined'
Copy the code

In log2, we explicitly declare the type of the function argument x as number, which means that the type of the function argument x can be either unpassed or number.

Note here: The default parameter type of the function must be a subtype of the parameter type

function log3(x: number | string = 'hello') {
    console.log(x);
}
Copy the code

Function of log3 optional function parameters of x type joint type number | string, but is because the default argument string type joint type number | string subtypes, so the TypeScript also will check through.

5.5 Remaining Parameters

In ES6, JavaScript supports residual parameters for function parameters, which can be collected into a single variable. Similarly, such parameter type definitions are supported in TypeScript

function sum(. nums: number[]) {
    return nums.reduce((a, b) = > a + b, 0);
}
sum(1.2); / / = > 3
sum(1.2.3); / / = > 6
sum(1.'2'); // ts(2345) Argument of type 'string' is not assignable to parameter of type 'number'
Copy the code

If we define the type of the function parameter nums polymerization for (number | string) []

function sum(. nums: (number | string)[]) :number {
    return nums.reduce<number>((a, b) = > a + Number(b), 0);
}
sum(1.'2'.3); / / 6
console.log(sum('1'.'2'.3));/ / 6
Copy the code

Function, then the type of each parameter is joint type number | string, so the sum (1, ‘2’, 3) the type of inspection is passed.

5.6 this

As you know, the reference to the function this has always been a headache in JavaScript. This is because the value of this cannot be determined until the function is called, not to mention that there are methods that can change the orientation of this. That is, the type of this is not fixed; it depends on the context in which it is executed.

With TypeScript, however, we don’t have to worry about this. By specifying the type of this (in strict mode, you must explicitly specify this), TypeScript will notify us when we misuse this, as shown in the following code:

function say() {
    console.log(this.name); // ts(2683) 'this' implicitly has type 'any' because it does not have a type annotation
}
say();
Copy the code

In the above code, if we call say directly, this should point to the global window or global (in Node). However, TypeScript in strict mode prompts that the type of this is any, which requires us to explicitly specify the type manually.

In TypeScript, we simply declare the object to which this refers (that is, how the function is called) in the first argument to a function, such as the simplest object method to which this refers

function say(this: Window, name: string) {
    console.log(this.name);
}
window.say = say;
window.say('hi');
const obj = {
    say
};
obj.say('hi'); // ts(2684) The 'this' context of type '{ say: (this: Window, name: string) => void; }' is not assignable to method's 'this' of type 'Window'.
Copy the code

When obj.say() is called, TypeScript detects that this is not pointing to window

say('captain'); // ts(2684) The 'this' context of type 'void' is not assignable to method's 'this' of type 'Window'
Copy the code

Note that if we call say() directly, this should actually refer to the global variable window, but TypeScript doesn’t know who called say, so it defaults to void, prompting a TS (2684) error.

At this point, we can avoid this error by calling window.say(), which is also a safe design. Because in JavaScript’s strict mode, the global scope function refers to this as undefined.

Similarly, when defining a function property on an object, TypeScript finds an error pointing to this if the actual call points to something different than the specified this. The example code looks like this:

interface Person {
    name: string;
    say(this: Person): void;
}
const person: Person = {
    name: 'captain'.say() {
        console.log(this.name); }};const fn = person.say;
fn(); // ts(2684) The 'this' context of type 'void' is not assignable to method's 'this' of type 'Person'
Copy the code

Note: Explicitly annotating the type this in a function, which appears to take the first parameter, doesn’t mean that the function actually has an extra parameter, because when TypeScript is translated into JavaScript, the “pseudo-parameter” this is erased. This is one of the few TypeScript syntax features.

Of course, this feature is confusing at first, so it’s important to keep it in mind. When the preceding say function is translated into JavaScript, this is erased

function say(name) {
    console.log(this.name);
}
Copy the code

In the same way, we can explicitly limit the type this in a function property of a class (see section 06 for an introduction to class), and TypeScript can detect incorrect usage

class Component {
  onClick(this: Component){}}const component = new Component();
interface UI {
  addClickListener(onClick: (this: void) = > void) :void;
}
const ui: UI = {
  addClickListener(){}}; ui.addClickListener(new Component().onClick); // ts(2345)
Copy the code

In addition, using this makes it easy to express the type in a chained invocation style library

class Container {
  private val: number;
  constructor(val: number) {
    this.val = val;
  }
  map(cb: (x: number) = > number): this {
    this.val = cb(this.val);
    return this;
  }
  log(): this {
    console.log(this.val);
    return this; }}const instance = new Container(1)
  .map((x) = > x + 1)
  .log() / / = > 2
  .map((x) = > x * 3)
  .log(); / / = > 6
Copy the code

Container () ¶ Container (); Container (); Container (); Container (); Container (); Container (); It can always be chained indefinitely.

5.7 Function Overloading

JavaScript is a dynamic language. It can have multiple types of arguments and return values for the same function. This is called function polymorphism.

function convert(x: string | number | null) :string | number| - 1{
    if (typeof x === 'string') {
        return Number(x);
    }
    if (typeof x === 'number') {
        return String(x);
    }
    return -1;
}
const x1 = convert('1'); // => string | number
const x2 = convert(1); // => string | number
const x3 = convert(null); // => string | number
Copy the code

So, is there a way to more accurately describe the type of function that is constrained by the type of parameter and return value? Yes, this is Function Overload. In the following example, lines 1-3 define three different lists of Function types and describe different return types for different parameter types, while line 4 starts with the implementation of the Function.

function convert(x: string) :number;
function convert(x: number) :string;
function convert(x: null): -1;
function convert(x: string | number | null) :any {
    if (typeof x === 'string') {
        return Number(x);
    }
    if (typeof x === 'number') {
        return String(x);
    }
    return -1;
}
const x1 = convert('1'); // => number
const x2 = convert(1); // => string
const x3 = convert(null); // -1
Copy the code

Note: Each member of the function overload list (i.e., lines 1 to 3 in the example) must be a subset of the function implementation (i.e., line 4 in the example), for example “function convert(x: string): number” is “Function convert(x: string): number” String | number | null) : any “subset.

When the convert function is called, TypeScript looks up the function overload list from top to bottom for the type that matches the input parameter type, and uses the first matching overload definition first. Therefore, we need to put the most accurate function overloads first.

interface P1 {
    name: string;
}
interface P2 extends P1 {
    age: number;
}
function convert(x: P1) :number;
function convert(x: P2) :string;
function convert(x: P1 | P2) :any {}
const x1 = convert({ name: "" } as P1); // => number
const x2 = convert({ name: "".age: 18 } as P2); // number
Copy the code

Since P2 inherits from P1, an argument of type P2 matches the first function overload as an argument of type P1, in which case x1 and x2 both return number.

function convert(x: P2) :string;
function convert(x: P1) :number;
function convert(x: P1 | P2) :any {}const x1 = convert({ name: ' ' } as P1); // => number
const x2 = convert({ name: ' '.age: 18 } as P2); // => string
Copy the code

We just need to switch the order of the function overload list, and the arguments of type P2 and P1 can match the correct function overload

5.8 Type Predicates (IS)

function isString(s) :s is string { // Type predicates
  return typeof s === 'string';
}
function isNumber(n: number{
  return typeof n === 'number';
}
function operator(x: unknown{
  if(isString(x)) { // ok The x type is reduced to string
  }
  if (isNumber(x)) { // ts(2345) Unknown cannot be assigned to number}}Copy the code

In the code above, where we added the type of the return value, we explicitly stated the type of the parameter in the format “parameter name + is + type”, thereby causing the type to shrink, so an important application scenario for type predicate functions is to implement custom type guards.

Type predicates can only be used to define custom type guards, essentially telling the engine to narrow down the type being guarded to the more explicit type specified by IS when the guard returns true.