TypeScript learning notes - type advanced

1, Type inference

In the previous learning process, we need to write type annotations when defining basic types of variables. Although this can clearly indicate the data type of variables, it is really troublesome to write. TS also helps us take this problem into consideration. In most cases, TS will automatically deduce the type of variable according to the context and assignment expression without specifying the type annotation. This ability is called type inference:

let str = 'this is string'; 
let num = 1; 
let bool = true; 
// Equivalent to
let str: string = 'this is string';
let num: number = 1; 
let bool: boolean = true;

Except for variables with initial assignment, function parameters with default values and return types of functions can be inferred:

 /** According to the type of the parameter, it is inferred that the type of the return value is also number */ 
  function add1(a: number, b: number) {   
    return a + b;  
  } 
  const x1= add1(1, 1); // It is inferred that the type of x1 is also number   
  
  /** Infer that the type of parameter b is number, and the type of return value is also number */ 
  function add2(a: number, b = 1) {  
    return a + b; 
  } 

If the variable is declared without simultaneous assignment, no matter what type of value is assigned later, it will be inferred as any type and can receive any type of value:

// Unassigned when declared. Type is any
let myFavoriteNumber;  
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

2, Type assertion

1. Concept

Type assertion means that in some cases, we are very clear about the variable type of a value, but from the type inference of TS, there may be multiple data types. At this time, we can forcibly specify the variable type of the value through type assertion, so that the value can be assigned to the variable of the corresponding type, for example:

// An array was initialized
const arrayNumber: number[] = [1, 2, 3, 4];
// We clearly know that there are numbers greater than 3, so the return value must be number 
// However, in TS's view, the return value may be number or undefined, so assigning directly to the number variable will report an error
const greaterThan2: number = arrayNumber.find(num => num > 2); // Prompt ts(2322)

// Add a type assertion to force the data type of the return value
const greaterThan2: number = arrayNumber.find(num => num > 2) as number;

There are two types of syntax for type assertion: < data type > data or data as data type, which have the same function, but angle brackets will cause syntax conflict in JSX of React. It is recommended to use as syntax:

let someValue: any = "this is a string";
// Angle bracket syntax
let strLength: number = (<string>someValue).length;
// as grammar
let strLength: number = (someValue as string).length;

2. Non null assertion

We can use the suffix expression operator! To assert that a value is a type that is neither null nor undefined. In short, it is to exclude null and undefined from the value field of a value:

// Define a variable so that its type is union type  
let may: null | undefined | string;
// Use non null assertions, excluding null and undefined, so it can only be string, so toString() can be used
may!.toString(); // ok
// Non null assertion values that are not used may be of multiple types. toString() cannot be used directly
may.toString(); // Error ts(2531)

3. Determine assignment assertion

In TS, if a variable is used when it is declared but not assigned, an error will be compiled. However, if we are very clear that this variable will be assigned, we can use the definite assignment assertion, that is, when declaring the variable, add one after the variable!, The TS compiler will no longer report errors:

// Declare a variable without assigning a value. Use the deterministic assignment assertion
let x!: number;
// Assign values in this method
initialize();
// However, the compiler does not know that it will assign a value. If it does not add a definite assignment assertion, an error will be reported
console.log(2 * x); // Ok

function initialize() { 
  x = 10;
}

3, Literal type

1. Concept

In TS, literal quantity can represent not only value, but also type, that is, literal type. That is, when declaring a variable, assign a value to the variable and set the type of the variable as a type. At present, three literal types are supported: string type, numeric type and boolean type:

// String literal type
let specifiedStr: 'this is string' = 'this is string';
// Numeric literal type
let specifiedNum: 1 = 1; 
// Boolean literal type
let specifiedBoolean: true = true;

In addition to the literal value of 'string' in the case of 'string', the literal value of 'string' in the above case is not equivalent to that of 'string':

// String literal type
let specifiedStr: 'this is string' = 'this is string'; 
// string type
  let str: string = 'any string'; 
// Type 'string' cannot be assigned to type 'this is string'
  specifiedStr = str; // ts(2322) 
// Type 'this is string' can be assigned to type 'string'
  str = specifiedStr; // ok

Numeric literal types are similar to Boolean literal types.

2. String literal type

In practical application, defining a single string literal type is not very useful. We usually form multiple literal types into a joint type to explicitly limit the value range of variables. When used in combination with a function, the parameter is limited to the specified literal type collection. During compilation, it will check whether the parameter is a member of the specified union type:

// Joint literal type
type Direction = 'up' | 'down';
// Limit the parameter type of the function
function move(dir: Direction) { 
  // ...
}
// Can only be up or ok
move('up'); // ok
// If it is another string, it will compile and report an error
move('right'); // ts(2345)

3. Numeric literal type and Boolean literal type

The usage is similar to that of string literal type. It is to more clearly limit the value range of variables, so that users must use the data of specific values:

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

4, Type widening

1. Concept

If the variables defined by let and var, the formal parameters of functions and the non read-only attributes of objects meet the conditions of type inference, the inferred type is the parent type after the widening of the literal type of the initial value. Since const is a constant and cannot be changed, the type will not be widened:

  let str = 'this is string'; // The type is broadened to string  
  let strFun = (str = 'this is string') => str; // The type is broadened to (STR?: String) = > string; 
  const specifiedStr = 'this is string'; // The declared constant type is' this is string ' 
  let str2 = specifiedStr; // The type is' string ' 

2. Restriction type widening

We can add the type annotation of the corresponding literal type to the variable to limit the type broadening:

// Add type annotation to restrict type widening. The type is' this is string ' 
const specifiedStr: 'this is string' = 'this is string'; 
// Even if the let definition is used, the type is' this is string '
let str2 = specifiedStr; 

3. Special type widening

In TS, if the variable defined through let and var is not marked with type annotation and assigned null or undefined, the type of this variable will be widened to any:

let x = null; // Type broadened to any  
let y = undefined; // Type broadened to any 

const z = null; // Constant type cannot be null 

let z2 = z; // The type is null 
let x2 = x; // The type is null 
let y2 = y; // The type is undefined

Note: in strict mode, in some older versions (2.0), null and undefined will not be widened to "any".

4. Type of object property

TS's widening algorithm will regard the internal attribute of the object as the variable assigned to the let keyword declaration, and then infer the type of its attribute. It also prevents us from adding properties to the object that do not exist at the time of the Declaration:

// Declaration object
const obj = {
  x: 1, // Type is expanded to number type
};
// Assign a value of type number
obj.x = 6;
// Assigning a string type will result in an error
obj.x = '6'; // Type '"6"' is not assignable to type 'number'.obj.x = '6';
// Add new attribute
obj.y = 8; // Property 'y' does not exist on type '{ x: number; }'.obj.y = 8

5, Type narrowing

1. Concept

In TS, the process of reducing the type of variables from a relatively broad set to a clear set by some means is type reduction.

2. Type guard

We can use the type guard combined with typeof to reduce the type of function parameters:

  let func = (anything: any) => {  
    // If the passed parameter is of string type, it will be reduced to string type when returned 
    if (typeof anything === 'string') {  
      return anything; // The type is string   
 	// If the passed parameter is of type number, it will be reduced to type number when returned 
    } else if (typeof anything === 'number') {  
      return anything; // The type is number  
    }  
    return null; 
  };

When using type guard, you need to pay attention to some special values. For example, the result of typeof null is object.

3. Process control + equivalence judgment

We can also reduce the union type through process control statements (including but not limited to if, ternary operator, switch branch, etc.) + equivalence judgment (= = =):

  // Define a union type
  type Goods = 'pen' | 'pencil' |'ruler'; 
	// Parameter is union type
  const getCost = (item: Goods) =>  {
    // Type reduction using equivalent judgment
    if (item === 'pen') {    
      return item; // The item type is reduced to 'pen'  
    } else if (item === 'pencil') {   
      return item; // The item type is reduced to 'pencil' 
    } else {  
      return item; // The item type is reduced to 'ruler'   
    } 
  }

4. Label Union

We can also define a type attribute for the type and narrow the type through the type attribute:

// Interface type (the next blog will talk about it)
interface UploadEvent { 
  // Define type
  type: "upload";
  filename: string; 
  contents: string;
}
// Interface type (the next blog will talk about it)
interface DownloadEvent {  
  type: "download"; 
  filename: string;
}

type AppEvent = UploadEvent | DownloadEvent;

function handleEvent(e: AppEvent) { 
  switch (e.type) {   
    case "download":  
      e; // Type is DownloadEvent    
      break;  
    case "upload":    
      e; // Type is UploadEvent   
      break;  
  }
}

6, Cross type

Cross type refers to the use of & to merge multiple types into one type that contains all the properties of the merged type. Obviously, if the original type and literal type are combined into a cross type, it makes no sense, because there is no data that can belong to multiple original types at the same time. The real use of cross type is to combine multiple interface types (which will be discussed in the next blog) into one type, so as to achieve an effect similar to interface inheritance:

// Merging the two object types makes the IntersectionType have id, name and age attributes at the same time
type IntersectionType = { id: number; name: string; } & { age: number }; 
// Type comments must be followed by id, name and age attributes
const mixed: IntersectionType = {  
  id: 1,   
  name: 'name',  
  age: 18
}

Cross type is equivalent to finding the union of types, but what happens if two interfaces have properties with the same name? There are many situations here: two attributes with the same name have the same type, two attributes with the same name have different and incompatible types, and two attributes with the same name have different types, but one is another subtype.

In the first case, if two attributes with the same name have the same type, it will not have much impact. Two attributes with the same name will be combined into one:

// Define a cross type  
type IntersectionTypeConfict = { id: number; name: string; }  & { age: number; name: string; };  
// After crossing, it is equivalent to {id: number; name: string; age: number}

When two attributes with the same name have different and incompatible types, for example, one is string and the other is number, the defined cross type is a useless type, because there can be no data belonging to both string and number, and the attributes with the same name of this cross type will become never type:

// Define a cross type. Because there are properties with the same name and different types are incompatible, the properties with the same name will become the never type  
type IntersectionTypeConfict = { id: number; name: string; }  & { age: number; name: number; };  
const mixedConflict: IntersectionTypeConfict = {  
    id: 1,  
    name: 2, // ts(2322) error, 'number' type cannot be assigned to 'never' type   
    age: 2  
  };

If the types of attributes with the same name are compatible, for example, one is number, the other is numeric literal type or the subtype of number, the attribute of the combined cross type will become a subtype with a small range:

// Define a cross type. Because there are properties with the same name and different types, but they are compatible, the property will become a subtype
type IntersectionTypeConfict = { id: number; name: 2; }   & { age: number; name: number; }; 
  let mixedConflict: IntersectionTypeConfict = {  
    id: 1,  
    name: 2, // ok 
    age: 2 
  }; 
  mixedConflict = {   
    id: 1,  
    name: 22, // '22' type cannot be assigned to '2' type   
    age: 2  
  };

Another special case is that the attribute with the same name is a complex data type, so the result of intersection is to merge the object members. The merging rules are the same as the type merging:

interface A {
  x:{d:true},
}
interface B { 
  x:{e:string},
}
interface C {  
  x:{f:number},
}
// After merging, the type becomes {x: {d: true,e: string,f: number}}
type ABC = A & B & C
let abc:ABC = { 
  x:{  
    d:true,  
    e:'',   
    f:666  
  }
}

Keywords: Front-end TypeScript

Added by amycrystal123 on Tue, 08 Mar 2022 13:03:40 +0200