TypeScript Tutorial (N)

weak and strong typing

Programming languages ​​can be divided into strongly typed languages ​​and weakly typed languages ​​according to whether the data types are fixed.

weakly typed language

Weakly typed language: Variables, function parameters, and function return values ​​have no type. A variable can receive any type of data, and a function can also return any type of data.

let n = 10;

n = "abc";

strongly typed language

Strongly typed language: Variables, function parameters, etc. all have fixed types. When declaring a variable, the type of the variable must be specified. A variable of a type can only receive data of this type.

int n = 10;
n = 10.0;   //错误
n = "qqqq"  //错误

Strongly typed languages: C/C#/Java, etc.

Weak type: Javascript/lua/python/php etc.

Pros and cons of weakly typed languages:

  • Advantages: easy to use and flexible. No extensive type conversions are required.
  • Disadvantages: Not rigorous enough to judge the type of a variable or parameter from the code, which is not conducive to the development of large-scale projects by multiple people.

Pros and cons of strongly typed languages:

  • Advantages: rigorous logic, definite type, suitable for developing large-scale projects.
  • Disadvantages: During the development process, you need to pay attention to the type of data at all times, and you need to manually convert data between different types when calculating.

TypeScript

TypeScript is called TS for short.

The relationship between TS and JS

  • The relationship between TS and JS is actually the relationship between Less/Sass and CSS
  • Just like Less/Sass extends CSS, TS also extends JS
  • Just like Less/Sass will eventually be converted to CSS, the TS code we write will eventually be converted to JS

Introduction to TypeScript

TS is a superset (extension set) of JS. It implements strong typing on the basis of JS grammar, that is to say, TS is strongly typed JS, TS extends JS, and has things that JS does not have. TS is just a syntax standard, it has no execution environment, and TS code will eventually be converted into JS code and run in the JS execution environment.

  1. TS is a superset of JS.
  2. TS implements strong typing on the basis of JS syntax.
  3. TS extends JS, introduces the concept of types to JS, and adds many new features.
  4. TS code needs to be compiled into JS by a compiler, and then executed by a JS parser.
  5. TS is fully compatible with JS, in other words, any JS code can be directly used as TS.
  6. Compared with JS, TS has static typing, stricter syntax, and more powerful functions;
  7. TS can complete the code inspection before the code is executed, reducing the chance of runtime exceptions;
  8. TS code can be compiled into any version of JS code, which can effectively solve the compatibility problem of different JS operating environments;
  9. For the same function, the amount of code in TS is larger than that in JS, but because the code structure of TS is clearer and the variable types are clearer, TS is far better than JS in later code maintenance.

Why do you need TypeScript?

  • Simply put, because JavaScript is weakly typed, many errors are only discovered at runtime
  • And TypeScript provides a set of static detection mechanisms that can help us find errors at compile time

TypeScript Features

  • Support for the latest JavaScript features
  • Support code static checking
  • Supports features in backend languages ​​such as C, C++, Java, Go, etc. (enums, generics, type conversions, namespaces, declaration files, classes, interfaces, etc.)

official website

https://www.tslang.cn/index.html

TypeScript development environment construction

Install TS

Download the TS compiler, which can convert TS files to JS files.

npm install -g typescript

Compile TS

Manual compilation: Use the tsc command to convert a TS file into a JS file.

tsc index.ts   
//如果报错ts-node : 无法将“ts-node”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。
//可以试试 npx tsc index.ts   这种运行成功后如果需要使用Code Runner插件的话,需要将插件运行命令更换为npx开头,往下看有更换的步骤

Automatic compilation: When compiling a file, after using the -w command, the TS compiler will automatically monitor the change of the file and recompile the file when the file changes.

tsc index.ts -w  
//如果不行的话可以试试 npx tsc index.ts -w

Run TS in vsCode

Run TS files in vsCode, or run JS files directly

  • VSCode installs the Code Runner plugin

  • Install the ts-node module globally: npm i ts-node -g

  • Use the right key Run Codeor shortcut key alt+ctrl+nto run the TS file (remember here that the project path cannot contain Chinese characters, otherwise the path cannot be found, or an error will be reported directly)

    (If the output code has Chinese garbled characters, you can go to the Code Runner plug-in settings search Run in Terminal to select it)

    (If the path does not contain Chinese or an error is reported, just find setting.jsonan edit point in the plug-in settings, find code-runner.executorMapthe option, find typescriptthe value and replace it with npx ts-nodeSave, so that it will run according to the specified command when you right-click to run)

type of data

type declaration

  • Type declaration is a very important feature of TS

  • The type of variables (parameters, formal parameters) in TS can be specified by type declaration

  • After specifying the type, when assigning a value to the variable, the TS compiler will automatically check whether the value conforms to the type declaration, assign the value if it matches, or report an error

  • In short, a type declaration sets a type for a variable so that it can only store values ​​of a certain type

let 变量: 类型;

let 变量: 类型 = 值;

function fn(参数: 类型, 参数: 类型): 类型{
    ...
}

type

type example describe
number 1, -33, 2.5 any number
string 'Hihi",hi You can use double quotes, single quotes, template strings to represent strings
boolean true、false boolean true or false
null null null has its own type called null
undefined undefined undefined has its own type called undefined
object {name:'Zhang San'} any JS object
array [1,2,3] Arbitrary JS array
tuple [4,5] Tuple, TS new type, fixed-length array
any * any type
Literal itself The value of the restricted variable is the value of the literal
void Null value (undefined) no value (or undefined)
never no value cannot be any value
unknown * type-safe any
enum enum{A, B} Enumeration, a new type in TS

basic type

let str: string = "张三";
let num: number = 18;
let bool: boolean = false;
let u: undefined = undefined;
let n: null = null;
let obj: object = {name:'张三'};
let big: bigint = 100n;
let sym: symbol = Symbol("foo"); 

Array

There are two ways to define the array type: use Array<类型>or 类型[]two ways

let names: Array<string> = ['tom', 'lily', 'lucy'];
let ages: number[] = [22, 23, 24];

Define an array of union types

// 表示定义了一个名称叫做arr的数组, 
// 这个数组中将来既可以存储数值类型的数据, 也可以存储字符串类型的数据
let arr:(number | string)[];
arr = [1, 'b', 2, 'c'];

Define an array of specified object members:

// interface是接口
interface Arrobj{
    
    
    name:string,
    age:number
}
let arr:Arrobj[] = [{
    
    name:'jimmy',age:22}];

Tuple (tuple)

Yuan ancestor definition

As we all know, arrays are generally composed of values ​​of the same type, but sometimes we need to store values ​​of different types in a single variable, then we can use tuples. There are no tuples in JavaScript, tuples are a type specific to TypeScript that work like arrays.

The most important feature of tuples is that they can be restricted 数组元素的个数和类型, which is especially suitable for implementing multi-value returns.

Yuan ancestors are used to save data of fixed-length and fixed data types

let x: [string, number]; 
// 类型必须匹配且个数必须为2

x = ['hello', 10]; // OK 
x = ['hello', 10, 10]; // Error 
x = [10, 'hello']; // Error

Note that the tuple type can only represent an array with a known number and type of elements, the length has been specified, and an error will be prompted for out-of-bounds access. If there may be multiple types in an array, the number and type are uncertain, then directlyany[]

Destructuring assignment of tuple types

We can access the elements in the tuple by subscripting, which is not so convenient when there are many elements in the tuple. In fact, tuples also support destructuring assignment:

let employee: [number, string] = [1, "Semlinker"];
let [id, username] = employee;
console.log(`id: ${
      
      id}`);
console.log(`username: ${
      
      username}`);

After the above code runs successfully, the console will output the following message:

id: 1
username: Semlinker

It should be noted here that when destructuring and assigning, if the number of destructured array elements cannot exceed the number of elements in the tuple, otherwise errors will also occur, such as:

let employee: [number, string] = [1, "Semlinker"];
let [id, username, age] = employee;

In the above code, we added an age variable, but at this point the TypeScript compiler will prompt the following error message:

Tuple type '[number, string]' of length '2' has no element at index '2'.

Obviously the length [number, string]of is 2that there is no element 2at .

optional elements of tuple type

With the function signature type, when defining the tuple type, we can also declare the optional elements of the tuple type through ?the symbol . The specific example is as follows:

let optionalTuple: [string, boolean?];
optionalTuple = ["Semlinker", true];
console.log(`optionalTuple : ${
      
      optionalTuple}`);
optionalTuple = ["Kakuqo"];
console.log(`optionalTuple : ${
      
      optionalTuple}`);

In the above code, we define a variable optionalTuplenamed , which is required to contain a required string attribute and an optional Boolean attribute. After the code runs normally, the console will output the following:

optionalTuple : Semlinker,true
optionalTuple : Kakuqo

So what does declaring optional tuple elements do in practice? Here we give an example. In a three-dimensional coordinate axis, a coordinate point can (x, y, z)be expressed in the form of . For a two-dimensional coordinate axis, a coordinate point can (x, y)be expressed in the form of . For a one-dimensional coordinate axis, as long as (x)Use the form to express it. For this situation, in TypeScript, the characteristics of optional elements of tuple type can be used to define a coordinate point of tuple type. The specific implementation is as follows:

type Point = [number, number?, number?];

const x: Point = [10]; // 一维坐标点
const xy: Point = [10, 20]; // 二维坐标点
const xyz: Point = [10, 20, 10]; // 三维坐标点

console.log(x.length); // 1
console.log(xy.length); // 2
console.log(xyz.length); // 3

the remaining elements of the tuple type

The last element in the tuple type can be the remaining elements, in the form of ...X, here Xis the array type. The remaining elements representing the tuple type are open to zero or more additional elements. For example, to represent a tuple type [number, ...string[]]with one numberelement and any number of elements of type. stringFor better understanding, let's take a concrete example:

type RestTupleType = [number, ...string[]];
let restTuple: RestTupleType = [666, "Semlinker", "Kakuqo", "Lolo"];
console.log(restTuple[0]);
console.log(restTuple[1]);

read-only tuple type

TypeScript 3.4 also introduces new support for read-only tuples. We can prefix any tuple type with readonlythe keyword to make it a read-only tuple. Specific examples are as follows:

const point: readonly [number, number] = [10, 20];

After using readonlythe keyword to modify the tuple type, any operation that attempts to modify the elements in the tuple will throw an exception:

// Cannot assign to '0' because it is a read-only property.
point[0] = 1;
// Property 'push' does not exist on type 'readonly [number, number]'.
point.push(0);
// Property 'pop' does not exist on type 'readonly [number, number]'.
point.pop();
// Property 'splice' does not exist on type 'readonly [number, number]'.
point.splice(1, 1);

null and undefined

By default nulland undefinedare subtypes of all types. That means you can assign nulland undefinedto other types.

// null和undefined赋值给string
let str:string = "666";
str = null
str= undefined

// null和undefined赋值给number
let num:number = 666;
num = null
num= undefined

// null和undefined赋值给object
let obj:object ={};
obj = null
obj= undefined

// null和undefined赋值给Symbol
let sym: symbol = Symbol("foo"); 
sym = null
sym= undefined

// null和undefined赋值给boolean
let isDone: boolean = false;
isDone = null
isDone= undefined

// null和undefined赋值给bigint
let big: bigint =  100n;
big = null
big= undefined

If you specify in tsconfig.json "strictNullChecks":true, nulland undefinedcan only be assigned to voidand their respective types.

number and bigint

Although numberand bigintboth represent numbers, the two types are not compatible.

let big: bigint = 100n;
let num: number = 6;
big = num;
num = big;

will throw a type incompatible ts(2322) error.

void

voidIndicates that there is no type, and it is an equal relationship with other types, and cannot be directly assigned:

let a: void; 
let b: number = a; // Error

You can only assign nulland undefined(when strictNullChecksnot specified as true) to it. It is not very useful to declare voida variable of a type, and we generally only declare it when the function does not return a value.

It's worth noting that the method will get no return value undefined, but we need to define it as voida type, not undefineda type. Otherwise an error will be reported:

function fun(): undefined {
    
    
  console.log("this is TypeScript");
};
fun(); // Error

never

neverTypes represent the types of values ​​that never exist.

There are two cases where the value will never exist:

  1. If a function throws an exception during execution, then this function will never have a return value (because throwing an exception will directly interrupt the program running, which makes the program run until the step of returning the value, that is, it has an unreachable end point, and it will never return. does not exist returned);
  2. The code that executes an infinite loop ( infinite loop ) in the function makes the program never run to the step where the function returns the value, and there is never a return.
// 异常
function err(msg: string): never {
    
     // OK
  throw new Error(msg); 
}

// 死循环
function loopForever(): never {
    
     // OK
  while (true) {
    
    };
}

neverType, like nulland undefined, is also a subtype of any type, and can also be assigned to any type.

But no type is nevera subtype of or is assignable to nevera type (other than neveritself), anynot even tonever

let ne: never;
let nev: never;
let an: any;

ne = 123; // Error
ne = nev; // OK
ne = an; // Error
ne = (() => {
    
     throw new Error("异常"); })(); // OK
ne = (() => {
    
     while(true) {
    
    } })(); // OK

In TypeScript, you can use the characteristics of the never type to implement comprehensive checks. The specific examples are as follows:

type Foo = string | number;

function controlFlowAnalysisWithNever(foo: Foo) {
    
    
  if (typeof foo === "string") {
    
    
    // 这里 foo 被收窄为 string 类型
  } else if (typeof foo === "number") {
    
    
    // 这里 foo 被收窄为 number 类型
  } else {
    
    
    // foo 在这里是 never
    const check: never = foo;
  }
}

Note that in the else branch, we assign foo, narrowed to never, to an explicitly declared never variable. If all the logic is correct, then it should be able to compile here. But suppose one day your colleague modifies the type of Foo:

type Foo = string | number | boolean;

However, he forgot to modify the control flow in controlFlowAnalysisWithNeverthe method . At this time, the foo type of the else branch will be narrowed to booleanthe type, so that it cannot be assigned to the never type, and a compilation error will occur. In this way, we can ensure that controlFlowAnalysisWithNeverthe method always exhausts all possible types of Foo. Through this example, we can draw a conclusion: use never to avoid the occurrence of new joint types without corresponding implementations, and the purpose is to write absolutely type-safe code.

any

In TypeScript, any type can be classified as any type. This makes the any type the top-level type of the type system.

If it is a normal type, changing the type during assignment is not allowed:

let a: string = 'seven';
a = 7;
// TS2322: Type 'number' is not assignable to type 'string'.

But if it is anyof type , it is allowed to be assigned to any type.

let a: any = 666;
a = "Semlinker";
a = false;
a = 66
a = undefined
a = null
a = []
a = {
    
    }

Accessing any property on any is allowed, as is calling any method.

let anyThing: any = 'hello';
console.log(anyThing.myName);
console.log(anyThing.myName.firstName);
let anyThing: any = 'Tom';
anyThing.setName('Jerry');
anyThing.setName('Jerry').sayHello();
anyThing.myName.setFirstName('Cat');

If a variable is declared without specifying its type, it will be recognized as any value type :

let something;
something = 'seven';
something = 7;
something.setName('Tom');

Equivalent to

let something: any;
something = 'seven';
something = 7;
something.setName('Tom');

In many scenarios, this is too loose. Using anytypes , it's easy to write code that is correctly typed but has problems at runtime. If we use anythe type , we can't use a lot of protection mechanisms provided by TypeScript. Remember, any 是魔鬼!try not to use any.

To solve the problems anycaused by , TypeScript 3.0 introduces unknownthe type.

unknown

unknownAs anywith , all types are assignable to unknown:

let notSure: unknown = 4;
notSure = "maybe a string instead"; // OK
notSure = false; // OK

unknownThe biggest difference with anyis:

  • Values ​​of any type can be assigned to any, and anyvalues ​​of any type can be assigned to any type.
  • Values ​​of any type can be assigned to unknown, but unknowncan only be assigned to unknownand anytypes, and unknowncannot be assigned to other types
let notSure: unknown = 4;
let uncertain: any = notSure; // OK

let notSure: any = 4;
let uncertain: unknown = notSure; // OK

let notSure: unknown = 4;
let uncertain: number = notSure; // Error

unknownYou can't do anything with a type without narrowing it:

function getDog() {
    
    
 return '123'
}
 
const dog: unknown = {
    
    hello: getDog};
dog.hello(); // Error

This mechanism is very preventive and safer, which requires us to narrow down the type. We can use methods such as typeof, 类型断言and so on to narrow down the unknown range:

function getDogName() {
    
    
 let x: unknown;
 return x;
};
const dogName = getDogName();
// 直接使用
const upName = dogName.toLowerCase(); // Error
// typeof
if (typeof dogName === 'string') {
    
    
  const upName = dogName.toLowerCase(); // OK
}
// 类型断言 
const upName = (dogName as string).toLowerCase(); // OK

Number、String、Boolean、Symbol

First of all, let's review the Number, String, Boolean, and Symbol types that are easily confused with the primitive types number, string, boolean, and symbol when we first learn TypeScript. The latter are the corresponding primitive types, let's call 包装对象them for the object type.

From the perspective of type compatibility, the primitive type is compatible with the corresponding object type, and the object type is incompatible with the corresponding primitive type.

Let's look at a specific example:

let num: number;
let Num: Number;
Num = num; // ok
num = Num; // ts(2322)报错

In line 3 of the example, we can assign number to type Number, but assigning Number to number in line 4 will prompt ts(2322) error.

Therefore, we need to keep in mind not to use object type to annotate the type of value, because it doesn't make any sense.

object、Object 和 {}

In addition, object (lowercase initial letter, hereinafter referred to as "small object"), Object (capital initial letter, hereinafter referred to as "big Object") and {} (hereinafter referred to as "empty object")

The small object represents all non-primitive types, that is to say, we cannot assign primitive types such as number, string, boolean, and symbol to object. In strict mode, nulland undefinedtypes are also not assignable to object.

The following types are considered primitive types in JavaScript: string, boolean, number, bigint, symbol, nulland undefined.

Let's look at a concrete example:

let lowerCaseObject: object;
lowerCaseObject = 1; // ts(2322)
lowerCaseObject = 'a'; // ts(2322)
lowerCaseObject = true; // ts(2322)
lowerCaseObject = null; // ts(2322)
lowerCaseObject = undefined; // ts(2322)
lowerCaseObject = {
    
    }; // ok

Lines 2~6 in the example will prompt ts(2322) error, but after we assign an empty object to object in line 7, it can pass the static type detection.

Big Object represents all types with toString and hasOwnProperty methods, so all primitive types and non-primitive types can be assigned to Object. Likewise, in strict mode, null and undefined types cannot be assigned to Object.

Let's also look at a specific example below:

let upperCaseObject: Object;
upperCaseObject = 1; // ok
upperCaseObject = 'a'; // ok
upperCaseObject = true; // ok
upperCaseObject = null; // ts(2322)
upperCaseObject = undefined; // ts(2322)
upperCaseObject = {
    
    }; // ok

In the example, lines 2 to 4 and line 7 can pass static type detection, while lines 5 to 6 will prompt ts(2322) error.

As can be seen from the above example, the large Object contains primitive types, and the small object only contains non-primitive types, so the large Object seems to be the parent type of the small object. In fact, big Object is not only the parent type of small object, but also the subtype of small object.

Below we still use a specific example to illustrate.

type isLowerCaseObjectExtendsUpperCaseObject = object extends Object ? true : false; // true
type isUpperCaseObjectExtendsLowerCaseObject = Object extends object ? true : false; // true
upperCaseObject = lowerCaseObject; // ok
lowerCaseObject = upperCaseObject; // ok

In the example, the returned types of lines 1 and 2 are both true, and upperCaseObject and lowerCaseObject in lines 3 and 4 can be assigned to each other.

Note: Although the official document says that small objects can be used instead of large Objects, we still need to understand that large Objects are not completely equivalent to small objects.

The {} empty object type is the same as the big Object, which also represents a collection of primitive types and non-primitive types, and in strict mode, null and undefined cannot be assigned to {}, as shown in the following example:

let ObjectLiteral: {
    
    };
ObjectLiteral = 1; // ok
ObjectLiteral = 'a'; // ok
ObjectLiteral = true; // ok
ObjectLiteral = null; // ts(2322)
ObjectLiteral = undefined; // ts(2322)
ObjectLiteral = {
    
    }; // ok
type isLiteralCaseObjectExtendsUpperCaseObject = {
    
    } extends Object ? true : false; // true
type isUpperCaseObjectExtendsLiteralCaseObject = Object extends {
    
    } ? true : false; // true
upperCaseObject = ObjectLiteral;
ObjectLiteral = upperCaseObject;

In the example, the return types of lines 8 and 9 are both true, ObjectLiteral and upperCaseObject in lines 10 and 11 can be assigned to each other, and the assignment operations in lines 2~4 and 7 conform to static type detection ; and the 5th and 6th lines will prompt ts(2322) error.

In summary: {} and large Object are broader types (least specific) than small objects. {} and large Object can replace each other to represent primitive types (except null and undefined) and non-primitive types; while small objects represents a non-primitive type.

function

function declaration

  • When declaring a function, you need to specify the type of the parameter

  • A function with no return value can add void to indicate that the return value of the function is empty

  • The actual parameter passed when calling the function should also be consistent with the formal parameter type

function fn(x: number, y: number): void  {
    
    
 console.log(x + y);
}

function sum(x: number, y: number): number {
    
    
  return x + y;
}

sum(10, 20);

function expression

let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
    
    
  return x + y;
};

Define function types with interfaces

interface SearchFunc{
    
    
  (source: string, subString: string): boolean;
}

When using the function expression interface to define a function, the type restriction on the left side of the equal sign can ensure that the number of parameters, parameter types, and return value types remain unchanged when assigning a function name in the future.

optional parameters

function buildName(firstName: string, lastName?: string) {
    
    
  if (lastName) {
    
    
    return firstName + ' ' + lastName;
  } else {
    
    
    return firstName;
  }
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

Note: Required parameters are not allowed after optional parameters

parameter default value

function buildName(firstName: string, lastName: string = 'Cat') {
    
    
  return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

remaining parameters

function push(array: any[], ...items: any[]) {
    
    
  items.forEach(function(item) {
    
    
    array.push(item);
  });
}
let a = [];
push(a, 1, 2, 3);

function overloading

Since JavaScript is a dynamic language, we usually use different types of parameters to call the same function, and the function will return different types of call results according to different parameters:

function add(x, y) {
    
    
 return x + y;
}
add(1, 2); // 3
add("1", "2"); //"12"

Since TypeScript is a superset of JavaScript, the above code can be used directly in TypeScript, but when the TypeScript compiler noImplicitAnyopens configuration item, the above code will prompt the following error message:

Parameter 'x' implicitly has an 'any' type.
Parameter 'y' implicitly has an 'any' type.

This information tells us that the parameters x and y implicitly have anytype . To solve this problem, we can set a type for the parameter. Since we want addthe function to support both string and number types, we can define a string | numberunion type and give it an alias:

type Combinable = string | number;

After defining the Combinable union type, let's update addthe function :

function add(a: Combinable, b: Combinable) {
    
    
    if (typeof a === 'string' || typeof b === 'string') {
    
    
     return a.toString() + b.toString();
    }
    return a + b;
}

addAfter explicitly setting the type for the parameter of the function , the previous error message disappeared. So is addthe function , let's actually test it:

const result = add('Semlinker', ' Kakuqo');
result.split(' ');

In the above code, we use 'Semlinker'and ' Kakuqo'the two strings as parameters to call the add function, and save the result of the call to a variable resultnamed . At this time, we take it for granted that the type of the variable result is string at this time, so We can call splitthe method . But at this time, the TypeScript compiler has the following error message again:

Property 'split' does not exist on type 'number'.

numberObviously there is no splitproperty on objects of type . The problem is coming again, how to solve it? At this point we can take advantage of the function overloading feature provided by TypeScript.

Function overloading or method overloading is the ability to create multiple methods with the same name and different number or types of parameters. To solve the problems encountered above, the method is to provide multiple function type definitions for the same function for function overloading, and the compiler will process the function call according to this list.

type Types = number | string
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: Types, b: Types) {
    
    
  if (typeof a === 'string' || typeof b === 'string') {
    
    
    return a.toString() + b.toString();
  }
  return a + b;
}
const result = add('Semlinker', ' Kakuqo');
result.split(' ');

In the above code, we provide multiple function type definitions for the add function to realize function overloading. Afterwards, the nasty error message disappears again, because the result variable is now of stringtype type .

type inference

  • TS will automatically infer the type of the variable according to the context
  • When the declaration and assignment of variables are carried out at the same time, the TS compiler will automatically determine the type of the variable
  • If the variable is declared and assigned at the same time, the type declaration can be omitted
{
    
    
  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;
}

Looking at the above example, you may have been muttering: variables that define basic types need to write type annotations. TypeScript is too troublesome, right? In the example, when using let to define a variable, we just write the type annotation, after all, the value may be changed. However, when using constconstants you need to write type annotations, which is really troublesome.

In fact, TypeScript has long considered such a simple and obvious problem.

In many cases, TypeScript will automatically infer the type of the variable according to the context, without us needing to write type annotations. Therefore, the above example can be simplified to something like this:

{
    
    
  let str = 'this is string'; // 等价
  let num = 1; // 等价
  let bool = true; // 等价
}
{
    
    
  const str = 'this is string'; // 不等价
  const num = 1; // 不等价
  const bool = true; // 不等价
}

We call TypeScript's ability to infer types based on assignment expressions 类型推断.

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

{
    
    
  /** 根据参数的类型,推断出返回值的类型也是 number */
  function add1(a: number, b: number) {
    
    
    return a + b;
  }
  const x1= add1(1, 1); // 推断出 x1 的类型也是 number
  
  /** 推断参数 b 的类型是数字或者 undefined,返回值的类型也是数字 */
  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
}

If there is no assignment at the time of definition, regardless of whether there is an assignment afterwards, it will be inferred as anya type and will not be type checked at all:

let myFavoriteNumber;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

type assertion

Sometimes you'll run into a situation where you know more details about a value than TypeScript does. Usually this happens when you clearly know that an entity has a more specific type than it already has.

This is the way to tell the compiler, "trust me, I know what I'm doing" through type assertions. Type assertions are like type conversions in other languages, but without special data checking and destructuring. It has no runtime impact, it only works during the compile phase.

TypeScript type detection cannot be absolutely intelligent, after all, programs cannot think like humans. Sometimes we run into situations where we know the actual types better than TypeScript does, like in the following example:

const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find(num => num > 2); // 提示 ts(2322)

Among them, greaterThan2 must be a number (3 to be precise), because there are obviously members greater than 2 in arrayNumber, but static typing can't do anything to the logic at runtime.

From the perspective of TypeScript, the type of greaterThan2 may be either a number or undefined, so the above example prompts a ts(2322) error, and we cannot assign the type undefined to the type number at this time.

However, we can use a certain way - type assertion (similar to a type cast that only works at the type level) to tell TypeScript to do type checking in our way.

For example, we can use the as syntax to make type assertions, as shown in the following code:

const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find(num => num > 2) as number;

grammar

// 尖括号 语法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

// as 语法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

Although there is no difference between the above two methods, the angle bracket format will cause grammatical conflicts with JSX in react, so we recommend using the as syntax.

non-null assertion

In contexts where the type checker cannot determine the type, a new postfix expression operator !can be used to assert that the operand is of type non-null and non-undefined. Specifically, x! will exclude null and undefined from the x range.

See the following examples for details:

let mayNullOrUndefinedOrString: null | undefined | string;
mayNullOrUndefinedOrString!.toString(); // ok
mayNullOrUndefinedOrString.toString(); // ts(2531)
type NumGenerator = () => number;

function myFunc(numGenerator: NumGenerator | undefined) {
    
    
  // Object is possibly 'undefined'.(2532)
  // Cannot invoke an object which is possibly 'undefined'.(2722)
  const num1 = numGenerator(); // Error
  const num2 = numGenerator!(); //OK
}

sure assignment assertion

Allows you to place a !sign to tell TypeScript that the property will be explicitly assigned. To better understand what it does, let's look at a concrete example:

let x: number;
initialize();

// Variable 'x' is used before being assigned.(2454)
console.log(2 * x); // Error
function initialize() {
    
    
  x = 10;
}

Obviously, the exception message means that the variable x is used before the assignment. To solve this problem, we can use the assertion to determine the assignment:

let x!: number;
initialize();
console.log(2 * x); // Ok

function initialize() {
    
    
  x = 10;
}

With a let x!: number;definite assignment assertion, the TypeScript compiler knows that the property will definitely be assigned a value.

literal type

In TypeScript, literals can represent not only values, but also types, so-called literal types.

Currently, TypeScript supports 3 literal types: string literal type, numeric literal type, and Boolean literal type. The corresponding string literals, numeric literals, and Boolean literals have the same literal type as their values. Specific examples are as follows:

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

For example, 'this is string' (here means a string literal type) type is string type (exactly a subtype of string type), but the string type is not necessarily 'this is string' (here means a string literal type Quantity type) type, the following specific example:

{
    
    
  let specifiedStr: 'this is string' = 'this is string';
  let str: string = 'any string';
  specifiedStr = str; // ts(2322) 类型 '"string"' 不能赋值给类型 'this is string'
  str = specifiedStr; // ok 
}

For example, we use "horse" as a metaphor for the string type, that is, "dark horse" refers to the type of 'this is string'. "Dark horse" must be "horse", but "horse" is not necessarily "dark horse", it may still be "white horse". "Grey Horse". Therefore, the 'this is string' literal type can assign a value to the string type, but the string type cannot assign a value to the 'this is string' literal type. This metaphor is also suitable for describing the relationship between other literals such as numbers and Booleans and their parent classes. .

string literal type

Generally speaking, we can use a string literal type as the variable type, as shown in the following code:

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

In fact, it is not very useful to define a single literal type. Its real application scenario is that multiple literal types can be combined into a joint type (explained later), which is used to describe a practical collection with clear members .

As shown in the following code, we use the literal union type to describe a clear set that can be 'up' or 'down', so that we can clearly know the required data structure.

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'


By using the union type of literal type combination, we can restrict the parameters of the function to the specified literal type set, and then the compiler will check whether the parameter is a member of the specified literal type set.

Thus, using literal types (composed union types) allows function parameters to be restricted to more specific types than using string types. This not only improves the readability of the program, but also guarantees the parameter type of the function, which can be said to kill two birds with one stone.

Number Literal Types and Boolean Literal Types

The use of numeric literal types and Boolean literal types is similar to the use of string literal types. We can use the union type of literal combinations to limit the parameters of functions to more specific types, such as declaring a type Config as shown below :

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

In the above code, we limit the size attribute to be the string literal type 'small' | 'big', and the isEnable attribute to be the Boolean literal type true | false (the Boolean literal only contains true and false, the combination of true | false and There is no difference if you use boolean directly), and the margin attribute is a numeric literal type 0 | 2 | 4.

let and const analysis

Let's first look at a const example, as shown in the following code:

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

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

Next we look at the let example shown below:

{
    
    

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

In the above code, the type of the changeable variable of the default explicit type annotation is converted to the parent type of the assignment literal type, for example, the type of str is 'this is string' type (here represents a string literal type) The parent type is string, and the type of num is the parent type number of type 1.

This design conforms to programming expectations, which means that we can assign str and num any value (as long as the type is a variable of a subset of string and number):

  str = 'any string';
  num = 2;
  bool = false;

We call this design of converting TypeScript’s literal quantum type to the parent type “literal widening”, which is the widening of the literal type. For example, the string literal type mentioned in the above example is converted to the string type. Below we Let me introduce it emphatically.

Type Widening

All variables defined by let or var, formal parameters of functions, and non-read-only properties of objects, if they meet the conditions of specifying initial values ​​and not explicitly adding type annotations, then their inferred types are the specified initial value literals The type after type widening is literal type widening.

Let's understand the widening of literal types through the example of string literals:

  let str = 'this is string'; // 类型是 string
  let strFun = (str = 'this is string') => str; // 类型是 (str?: string) => string;
  const specifiedStr = 'this is string'; // 类型是 'this is string'
  let str2 = specifiedStr; // 类型是 'string'
  let strFun2 = (str = specifiedStr) => str; // 类型是 (str?: string) => string;

Because lines 1 and 2 satisfy the conditions of let and formal parameters without explicitly declaring type annotations, the types of variables and formal parameters are expanded to string (the formal parameter type is exactly string | undefined).

Because the constant in line 3 cannot be changed, the type is not widened, so the type of specifiedStr is 'this is string' literal type.

Lines 4~5, because the assigned value specifiedStr is a literal type, and there is no explicit type annotation, so the types of variables and formal parameters are also widened. In fact, such a design meets the actual programming demands. Let's imagine that if the type of str2 is inferred as 'this is string', it will be immutable, because assigning any other value of type string will prompt a type error.

Based on the conditions of literal type widening, we can control the type widening behavior by adding explicit type annotations through the code shown below.

{
    
    
  const specifiedStr: 'this is string' = 'this is string'; // 类型是 '"this is string"'
  let str2 = specifiedStr; // 即便使用 let 定义,类型是 'this is string'
}

In fact, in addition to literal type widening, TypeScript also has a design similar to "Type Widening" (type widening) for certain specific types of values. Let's take a look at it in detail below.

For example, to widen the types of null and undefined, if the variables defined by let and var satisfy the undeclared type annotation and are assigned the value of null or undefined, the type of these variables is inferred to be any:

{
    
    
  let x = null; // 类型拓宽成 any
  let y = undefined; // 类型拓宽成 any

  /** -----分界线------- */
  const z = null; // 类型是 null

  /** -----分界线------- */
  let anyFun = (param = null) => param; // 形参类型是 null
  let z2 = z; // 类型是 null
  let x2 = x; // 类型是 null
  let y2 = y; // 类型是 undefined
}


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

In order to understand type widening more conveniently, let's take an example and analyze it more deeply.

Suppose you are writing a vector library, you first define a Vector3 interface, and then define the getComponent function to get the value of the specified axis:

interface Vector3 {
    
    
  x: number;
  y: number;
  z: number;
}

function getComponent(vector: Vector3, axis: "x" | "y" | "z") {
    
    
  return vector[axis];
}

However, when you try to use the getComponent function, TypeScript will prompt the following error message:

let x = "x";
let vec = {
    
     x: 10, y: 20, z: 30 };
// 类型“string”的参数不能赋给类型“"x" | "y" | "z"”的参数。
getComponent(vec, x); // Error

Why does the above error occur? We know from TypeScript's error message that the type of the variable x is inferred to be of type string, and the getComponent function expects a more specific type for its second parameter. This was widened in practice, so it resulted in a bug.

This process is complicated because there are many possible types for any given value. For example:

const arr = ['x', 1];

What should be the type of the above arr variable? Here are some possibilities:

  • (‘x’ | 1)[]
  • [‘x’, 1]
  • [string, number]
  • readonly [string, number]
  • (string | number)[]
  • readonly (string|number)[]
  • [any, any]
  • any[]

Without more context, TypeScript has no way of knowing which type is "correct", it has to guess your intent. As smart as TypeScript is, it can't read your mind. It's not guaranteed to be 100% correct, as we just saw with inadvertent errors.

In the following example, the type of the variable x is inferred to be string because TypeScript allows such code:

let x = 'semlinker';
x = 'kakuqo';
x = 'lolo';

For JavaScript, the following code is also legal:

let x = 'x';
x = /x|y|z/;
x = ['x', 'y', 'z'];

TypeScript tries to strike a balance between specificity and flexibility when inferring the type of x to be string. The general rule is that the type of a variable should not change after declaration, so string makes more sense than string|RegExp or string|string[] or any string.

TypeScript provides a few ways to control the widening process. One of the ways is to use const. If a variable is declared const instead of let, its type will be narrower. In fact, using const can help us fix the error in the previous example:

const x = "x"; // type is "x" 
let vec = {
    
     x: 10, y: 20, z: 30 };
getComponent(vec, x); // OK

Because x cannot be reassigned, TypeScript can infer the narrower type and there will be no errors in subsequent assignments. Because the string literal "x" is assignable to "x"|"y"|"z", the code passes the type checker.

However, const 并不是万灵药。对于对象和数组,仍然会存在问题.

The following code is fine in JavaScript:

const obj = {
    
     
  x: 1,
}; 

obj.x = 6; 
obj.x = '6';

obj.y = 8;
obj.name = 'semlinker';

Whereas in TypeScript, for obj's type, it can be {readonly x:1}of type , or the more general {x:number}type of . Of course, it may also be of type {[key: string]: number}or object. For objects, TypeScript's widening algorithm treats its internal properties as assigning them to variables declared with the let keyword, and then infers the types of its properties. So obj is of type {x:number}. This allows you to assign obj.x to other variables of type number instead of variable of type string, and it also prevents you from adding other attributes.

So the last three lines of the statement will throw an error:

const obj = {
    
     
  x: 1,
};

obj.x = 6; // OK 


// Type '"6"' is not assignable to type 'number'.
obj.x = '6'; // Error

// Property 'y' does not exist on type '{ x: number; }'.
obj.y = 8; // Error

// Property 'name' does not exist on type '{ x: number; }'.
obj.name = 'semlinker'; // Error

TypeScript tries to strike a balance between specificity and flexibility. It needs to infer a type specific enough to catch the error, but not the type of the error. It infers the type of a property from its initial value, and of course there are several ways to override TypeScript's default behavior. One is to provide explicit type annotations:

// Type is { x: 1 | 3 | 5; }
const obj: {
    
     x: 1 | 3 | 5 } = {
    
    
  x: 1 
};

Another approach is to use const assertions. Not to be confused with let and const, which introduce symbols in the value space. This is a pure type-level construct. Let's look at the different inferred types for the following variables:

// Type is { x: number; y: number; }
const obj1 = {
    
     
  x: 1, 
  y: 2 
}; 

// Type is { x: 1; y: number; }
const obj2 = {
    
    
  x: 1 as const,
  y: 2,
}; 

// Type is { readonly x: 1; readonly y: 2; }
const obj3 = {
    
    
  x: 1, 
  y: 2 
} as const;

When you use a const assertion after a value, TypeScript will infer the narrowest type for it, no widening. For real constants, this is usually what you want. Of course you can also use const assertions on arrays:

// Type is number[]
const arr1 = [1, 2, 3]; 

// Type is readonly [1, 2, 3]
const arr2 = [1, 2, 3] as const;

Since there is type widening, there will naturally be type narrowing. Let's briefly introduce Type Narrowing.

Type Narrowing

In TypeScript, we can narrow the type of variables from a relatively broad set to a relatively small and clear set through certain operations, which is "Type Narrowing".

For example, we can use type guards (described later) to narrow the type of function parameters from any to specific types, as follows:

{
    
    
  let func = (anything: any) => {
    
    
    if (typeof anything === 'string') {
    
    
      return anything; // 类型是 string 
    } else if (typeof anything === 'number') {
    
    
      return anything; // 类型是 number
    }
    return null;
  };
}

In VS Code, the prompt type of the anything variable from hover to line 4 is string, and the prompt type to line 6 is number.

Similarly, we can use type guards to narrow down union types to explicit subtypes, as shown in the following example:

{
    
    
  let func = (anything: string | number) => {
    
    
    if (typeof anything === 'string') {
    
    
      return anything; // 类型是 string 
    } else {
    
    
      return anything; // 类型是 number
    }
  };
}

Of course, we can also converge the joint type to a more specific type through literal type equivalent judgment (===) or other control flow statements (including but not limited to if, ternary operator, switch branch), as shown in the following code Show:

{
    
    
  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'
    }
  }
}

In the above getCost function, the accepted parameter type is the joint type of literal type, and the function contains three process branches of ifthe statement , and the parameters of the function called by each process branch are specific and independent literal types.

Then why can a variable item whose type consists of multiple literals be passed to a function that only accepts a single specific literal type getPenCost、getPencilCost、getRulerCost? This is because in each flow branch, the compiler knows what type of item is in the flow branch. For example, if item === branch of 'pencil', the type of item is shrunk to "pencil".

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

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

In general TypeScriptis very good at identifying types by condition, but be careful when dealing with some special values ​​- it may contain things you don't want! For example, the following method of excluding null from a union type is an error:

const el = document.getElementById("foo"); // Type is HTMLElement | null
if (typeof el === "object") {
    
    
  el; // Type is HTMLElement | null
}

Since typeof nullin the result is "object", you're not actually excluding nullvalues ​​from this check. Besides that, raw values ​​of falsy create similar problems:

function foo(x?: number | string | null) {
    
    
  if (!x) {
    
    
    x; // Type is string | number | null | undefined\
  }
}

Since the empty string and 0 are falsy values, the type of x in the branch may be string or number. Another common way to help the type checker narrow down types is to put an explicit "label" on them:

interface UploadEvent {
    
    
  type: "upload";
  filename: string;
  contents: string;
}

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;
  }
}

This pattern is also known as "tag union" or "discriminated union", and it has a wide range of applications in TypeScript.

union type

The union type indicates that the value can be one of multiple types, and each type |is separated .

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven'; // OK
myFavoriteNumber = 7; // OK

Union types are nulloften undefinedused with or :

const sayHello = (name: string | undefined) => {
    
    
  /* ... */
};

For example, namehere the type of is string | undefinedmeans stringthat undefinedthe value of or can be passed to sayHellothe function.

sayHello("semlinker"); 
sayHello(undefined);

From this example, you can intuit that the union of type A and type B results in a type that accepts both A and B values. Additionally, for union types, you may encounter the following usages:

let num: 1 | 2 = 1;
type EventNames = 'click' | 'scroll' | 'mousemove';

In the above example 1, 2or 'click'is called literal type, which is used to constrain the value to be only one of several values.

type alias

  • type: Type aliases are used to give a new name to a type, and the same type can be used multiple times with one name.
  • Type aliases are often used with union types.

type type alias syntax:

type Point = {
    
    
  x: number;
  y: number;
};

function printCoord(pt: Point) {
    
    
  console.log("The coordinate's x value is " + pt.x);
  console.log("The coordinate's y value is " + pt.y);
}

printCoord({
    
     x: 100, y: 100 });

Type aliases can be used to name any type, not just object types. Commonly used for union types.

type test = number; //基本类型
let num: test = 10;
type userOjb = {
    
    name:string} // 对象
type getName = ()=>string  // 函数
type data = [number,string] // 元组
type numOrFun = number | getName  // 联合类型

Note: Type aliasing, as the name suggests, means we just give the type a new name, not create a new type.

cross type

Intersecting types is combining multiple types into one type. This allows us to superimpose existing types into one type, which contains all the required characteristics of the type, using the &definition intersection type.

{
    
    
  type Useless = string & number;
}

Obviously, if we only merge atomic types such as primitive types, literal types, and function types into cross types, it is useless, because any type cannot satisfy multiple atomic types at the same time, such as both string type and number type. Therefore, in the above code, the type of the type alias Useless is never.

The real use of the cross type is to merge multiple interface types into one type, so as to achieve the effect of equivalent interface inheritance, which is the so-called merged interface type, as shown in the following code:

  type IntersectionType = {
    
     id: number; name: string; } & {
    
     age: number };
  const mixed: IntersectionType = {
    
    
    id: 1,
    name: 'name',
    age: 18
  }

In the above example, we use the intersection type to make the IntersectionType have all the attributes of id, name, and age at the same time. Here we can try to understand the merged interface type as a union set.

think

Here, let's think about it divergently: what will happen if there are attributes with the same name in multiple interface types that are merged?

If the types of attributes with the same name are incompatible, for example, in the above example, the type of the name attribute of the two interface types with the same name is number and the other is string. After merging, the type of the name attribute is the cross type of the two atomic types of number and string, that is never, as shown in the following code:

  type IntersectionTypeConfict = {
    
     id: number; name: string; } 
  & {
    
     age: number; name: number; };
  const mixedConflict: IntersectionTypeConfict = {
    
    
    id: 1,
    name: 2, // ts(2322) 错误,'number' 类型不能赋给 'never' 类型
    age: 2
  };

At this point, if we assign any type of name attribute value to mixedConflict, it will prompt a type error. And if we don't set the name attribute, it will prompt an error that the required name attribute is missing. In this case, it means that the IntersectionTypeConfict type intersected in the above code is a useless type.

If the types of attributes with the same name are compatible, for example, one is number and the other is a subtype of number or a literal type of number, the type of the name attribute after merging is the subtype of the two.

The type of the name attribute in the example shown below is the number literal type 2, so we cannot assign any value other than 2 to the name attribute.

  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' 类型不能赋给 '2' 类型
    age: 2
  };

So what happens if the attribute with the same name is a non-primitive data type. Let's look at a concrete example:

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

After the above code runs successfully, the following output will be output:

{
    
     x: {
    
    d: true, e: '', f: 666 } }

From the above results, we can see that when mixing multiple types, if there are the same members and the member types are non-basic data types, then the merge can be successful.

object oriented

object-oriented concepts

Object-oriented is a very important idea in programming. It is understood by many students as a difficult and esoteric problem, but it is not. Object-oriented is very simple. In short, all operations in the program need to be done through objects.

  • for example:
    • To operate the browser, use the window object
    • To operate a web page, use the document object
    • The operation console needs to use the console object

All operations must go through objects, which is the so-called object-oriented, so what exactly is an object? Let’s first talk about what a program is. The essence of a computer program is the abstraction of real things. The antonym of abstraction is concrete. For example, a photo is an abstraction of a specific person, and a car model is an abstraction of a specific car. A program is also an abstraction of things. In a program, we can represent a person, a dog, a gun, a bullet, and all other things. When a thing arrives in the program, it becomes an object.

All objects in the program are divided into two parts: data and functions. Taking people as an example, their name, gender, age, height, weight, etc. belong to data, and people can talk, walk, eat, and sleep, which belong to human functions. . Data are called properties in objects, and functions are called methods. So in short, everything is an object in a program.

class

If you want to be object-oriented and manipulate objects, you must first have objects, so the next question is how to create objects. To create an object, you must first define a class. The so-called class can be understood as the model of the object, and the object of the specified type can be created according to the class in the program.

For example: Human objects can be created through the Person class, dog objects can be created through the Dog class, and car objects can be created through the Car class. Different classes can be used to create different objects.

  • Define class:
class 类名 {
    
    
 属性名: 类型;

 constructor(参数: 类型){
    
    
   this.属性名 = 参数;
 }

 方法名(){
    
    
   ....
 }

}
  • Example:
class Person{
    
    
  name: string;
  age: number;

  constructor(name: string, age: number){
    
    
    this.name = name;
    this.age = age;
  }

  sayHello(){
    
    
    console.log(`大家好,我是${
      
      this.name}`);
  }
}
  • Use class:
const p = new Person('张三', 18);
p.sayHello();

Object Oriented Features

encapsulation

  • An object is essentially a container for attributes and methods, and its main function is to store attributes and methods, which is called encapsulation

  • By default, the properties of an object can be modified arbitrarily. In order to ensure data security, the permissions of properties can be set in TS

Read-only attribute (readonly):

  • If you add a readonly when declaring a property, the property becomes a read-only property and cannot be modified

Properties in TS have three modifiers:

  • public: public property (default value), can be accessed and modified anywhere: current class, subclasses and objects
  • protected: protected, can be accessed and modified in the current class and subclasses
  • private : Private property, which can only be accessed or modified in the current class

public

class Person{
    
    
  public name: string; // 写或什么都不写都是public
  public age: number;

  constructor(name: string, age: number){
    
    
    this.name = name; // 可以在类中修改
    this.age = age;
  }

  sayHello(){
    
    
    console.log(`大家好,我是${
      
      this.name}`);
  }
}

class Employee extends Person{
    
    
  constructor(name: string, age: number){
    
    
    super(name, age);
    this.name = name; //子类中可以修改
  }
}

const p = new Person('孙悟空', 18);
p.name = '猪八戒';// 可以通过对象修改

protected

class Person{
    
    
  protected name: string;
  protected age: number;

  constructor(name: string, age: number){
    
    
    this.name = name; // 可以修改
    this.age = age;
  }

  sayHello(){
    
    
    console.log(`大家好,我是${
      
      this.name}`);
  }
}

class Employee extends Person{
    
    

  constructor(name: string, age: number){
    
    
    super(name, age);
    this.name = name; //子类中可以修改
  }
}

const p = new Person('孙悟空', 18);
p.name = '猪八戒';// 不能修改

private

class Person{
    
    
  private name: string;
  private age: number;

  constructor(name: string, age: number){
    
    
    this.name = name; // 可以修改
    this.age = age;
  }

  sayHello(){
    
    
    console.log(`大家好,我是${
      
      this.name}`);
  }
}

class Employee extends Person{
    
    
  constructor(name: string, age: number){
    
    
    super(name, age);
    this.name = name; //子类中不能修改
  }
}

const p = new Person('孙悟空', 18);
p.name = '猪八戒';// 不能修改

attribute accessor

  • For some properties that do not want to be modified arbitrarily, they can be set to private

  • Setting it directly to private will result in no longer being able to modify its properties through the object

  • We can define a set of methods to read and set properties in a class. This kind of property that reads or sets properties is called a property accessor

  • The method of reading a property is called a setter method, and the method of setting a property is called a getter method

class Person{
    
    
  private _name: string;

  constructor(name: string){
    
    
    this._name = name;
  }

  get name(){
    
    
    return this._name;
  }

  set name(name: string){
    
    
    this._name = name;
  }

}

const p1 = new Person('孙悟空');
console.log(p1.name); // 通过getter读取name属性
p1.name = '猪八戒'; // 通过setter修改name属性

static property

  • Static properties (methods), also known as class properties. Using static properties does not need to create an instance, it can be used directly through the class
  • Static properties (methods) start with static
    • Without the static modifier, it is the properties and instance methods of the instance (object), these are the properties and methods of the instance
    • With the static modifier, there are static properties and static methods, which are the properties and methods of the class
class Tools{
    
    
  static PI = 3.1415926;

  static sum(num1: number, num2: number){
    
    
    return num1 + num2
  }
}

console.log(Tools.PI);
console.log(Tools.sum(123, 456));
  • this

    • In non-static methods (instance methods) this refers to the instance of the class
    • In a static method this refers to the class
  • Access to methods and properties

    • Instance attributes and instance methods can be accessed in non-static methods (instance methods), accessed through this
    • Static properties and static methods can also be used in non-static methods (instance methods), called by class names
    • In a static method, you cannot use instance attributes, and you cannot call instance methods
    • In a static method, you can call static properties and static methods
  • instance properties and static properties

    • Instance attributes and instance methods are called by the instance (object)
    • Static properties and static methods can only be called by class name

inherit

extends

  • Inheritance is yet another feature of object-oriented

  • Properties and methods from other classes can be introduced into the current class through inheritance

class Animal{
    
    
  name: string;

  constructor(name: string){
    
    
    this.name = name;

  }
}

class Dog extends Animal{
    
    
  age: number;
  constructor(name: string, age: number) {
    
     
    super(name); 
    this.age = age;
  }

  bark(){
    
    
    console.log(`${
      
      this.name}在汪汪叫!`);
  }
}

const dog = new Dog('旺财', 4);
dog.bark();

Extensions to a class can be done without modifying the class through inheritance.

Classes inherit properties and methods from base classes. Here, Dogis a derived class that derives from Animal the base class via extendsthe keyword . Derived classes are often called subclasses , and base classes are often called superclasses .

The derived class contains a constructor, which must be called super(), which executes the base class constructor. Moreover, we must call before accessing properties thisof . This is an important rule enforced by TypeScript.super()

rewrite

  • When inheritance occurs, if the method in the subclass replaces the method with the same name in the parent class, this is called method rewriting
class Animal{
    
    
  name: string;
  age: number;

  constructor(name: string, age: number){
    
    
    this.name = name;
    this.age = age;
  }

  run(){
    
    
    console.log(`父类中的run方法!`);
  }
}

class Dog extends Animal{
    
    

  bark(){
    
    
    console.log(`${
      
      this.name}在汪汪叫!`);
  }

  run(){
    
    
    super.run();
    console.log(`子类中的run方法,会重写父类中的run方法!`);
  }
}

const dog = new Dog('旺财', 4);
dog.bark();
  • Super can be used in subclasses to complete references to parent classes

abstract class

  • An abstract class is a class specially designed to be inherited by other classes, it can only be inherited by other classes and cannot be used to create instances
abstract class Animal{
    
    
  abstract run(): void;
  bark(){
    
    
    console.log('动物在叫~');
  }
}

class Dog extends Animals{
    
    
  run(){
    
    
    console.log('狗在跑~');
  }
}
  • A method beginning with abstract is called an abstract method. An abstract method has no method body and can only be defined in an abstract class. When inheriting an abstract class, the abstract method must be implemented.

interface

In TypeScript, we use interfaces (Interfaces) to define types of objects.

what is an interface

In object-oriented language, interface (Interfaces) is a very important concept. It is an abstraction of behavior, and how to act specifically needs to be implemented by classes (classes).

  • interface interface: defined specification
  • implements: Implementation: A class implements an interface, that is, implements the properties and methods defined by the interface

The interface in TypeScript is a very flexible concept. In addition to being used to [abstract a part of the behavior of the class], it is also often used to describe the "shape of the object (Shape)".

simple example

interface Person {
    
    
    name: string;
    age: number;
}
let tom: Person = {
    
    
    name: 'Tom',
    age: 25
};

In the above example, we defined an interface Person, and then defined a variable tomwhose type is Person. In this way, we constrain the shape tomof must Personbe consistent with the interface .

Interfaces are generally capitalized.

It is not allowed to define variables with fewer attributes than interfaces:

interface Person {
    
    
    name: string;
    age: number;
}
let tom: Person = {
    
    
    name: 'Tom'
};

// index.ts(6,5): error TS2322: Type '{ name: string; }' is not assignable to type 'Person'.
//   Property 'age' is missing in type '{ name: string; }'.

More attributes are also not allowed:

interface Person {
    
    
    name: string;
    age: number;
}

let tom: Person = {
    
    
    name: 'Tom',
    age: 25,
    gender: 'male'
};

// index.ts(9,5): error TS2322: Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'.
//   Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.

It can be seen that when assigning a value, the shape of the variable must be consistent with the shape of the interface .

optional | read-only property

interface Person {
    
    
  readonly name: string;
  age?: number;
}

Read-only properties are used to restrict the modification of its value only when the object is just created. In addition, TypeScript also provides ReadonlyArray<T>the type , which Array<T>is similar to except that all variable methods are removed, so it can ensure that the array cannot be modified after it is created.

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!

any attribute

Sometimes we want an interface to allow other arbitrary attributes in addition to mandatory and optional attributes. At this time, we can use the form of index signature to meet the above requirements.

interface Person {
    
    
    name: string;
    age?: number;
    [propName: string]: any;
}

let tom: Person = {
    
    
    name: 'Tom',
    gender: 'male'
};

It should be noted that once any attribute is defined, the type of both the definite attribute and the optional attribute must be a subset of its type

interface Person {
    
    
    name: string;
    age?: number;
    [propName: string]: string;
}

let tom: Person = {
    
    
    name: 'Tom',
    age: 25,
    gender: 'male'
};

// index.ts(3,5): error TS2411: Property 'age' of type 'number' is not assignable to string index type 'string'.
// index.ts(7,5): error TS2322: Type '{ [x: string]: string | number; name: string; age: number; gender: string; }' is not assignable to type 'Person'.
//   Index signatures are incompatible.
//     Type 'string | number' is not assignable to type 'string'.
//       Type 'number' is not assignable to type 'string'.

In the above example, the value of any attribute is allowed to be string, but the value ageof is number, notnumber a sub-attribute of , so an error is reported.string

In addition, it can be seen from the error message that the type { name: 'Tom', age: 25, gender: 'male' }of { [x: string]: string | number; name: string; age: number; gender: string; }, which is a combination of a joint type and an interface.

Only one arbitrary property can be defined in an interface. If you have properties of more than one type in the interface, you can use union types in any of the properties:

interface Person {
    
    
    name: string;
    age?: number; // 这里真实的类型应该为:number | undefined
    [propName: string]: string | number | undefined;
}

let tom: Person = {
    
    
    name: 'Tom',
    age: 25,
    gender: 'male'
};

duck typing

The so-called duck-type identification method is 像鸭子一样走路并且嘎嘎叫的就叫鸭子that it has the characteristics of a duck that it is a duck, that is, it is determined whether the object implements this interface by formulating rules.

example

interface LabeledValue {
    
    
  label: string;
}
function printLabel(labeledObj: LabeledValue) {
    
    
  console.log(labeledObj.label);
}
let myObj = {
    
     size: 10, label: "Size 10 Object" };
printLabel(myObj); // OK

interface LabeledValue {
    
    
  label: string;
}
function printLabel(labeledObj: LabeledValue) {
    
    
  console.log(labeledObj.label);
}
printLabel({
    
     size: 10, label: "Size 10 Object" }); // Error

In the above code, writing an object in the parameter is equivalent to directly assigning labeledObja value. This object has a strict type definition, so more or less parameters cannot be used. And when you use another variable to receive the object outside myObj, myObjit will not be checked for additional attributes, but it will be deduced according to the type let myObj: { size: number; label: string } = { size: 10, label: "Size 10 Object" };, and then this will be myObjassigned to labeledObj. At this time, according to the compatibility of the type, two types of objects, refer to duck The type identification method , because they both have labelattributes, are considered to be the same as two, so this method can be used to bypass redundant type checks.

Ways to bypass extra property checks

duck typing

As shown in the example above

type assertion

The meaning of type assertion is equivalent to telling the program that you know what you are doing, and the program will naturally not perform additional property checks.

interface Props {
    
     
  name: string; 
  age: number; 
  money?: number;
}

let p: Props = {
    
    
  name: "兔神",
  age: 25,
  money: -100000,
  girl: false
} as Props; // OK

index signature

interface Props {
    
     
  name: string; 
  age: number; 
  money?: number;
  [key: string]: any;
}

let p: Props = {
    
    
  name: "兔神",
  age: 25,
  money: -100000,
  girl: false
}; // OK

Case Analysis

Scenario: Declare a Child, Student, and Classroom classes respectively, and there is an instance method play in each class.

class Child {
    
    
	name: string;
	constructor(name: string) {
    
    
		this.name = name;
	}
	play():void {
    
    
		console.log(this.name + '正在玩耍');
	}
}

class Student {
    
    
	name: string;
	constructor(name: string) {
    
    
		this.name = name;
	}
	play():void {
    
    
		console.log(this.name + '正在玩耍');
	}
}

class Classroom {
    
    
	play(who: Child):void {
    
    
		who.play();
	}
}

let child: Child = new Child('小明');
let stu: Student = new Student('小刚');
let room: Classroom = new Classroom();
// 小孩可以在教室玩耍
room.play(child);
// 学生也可以在教室玩耍,但是Classroom的play方法限制只有Child类才能进去玩耍
// 如果类型不匹配就会报错,Classroom类不合理,肿么办???
// room.play(stu);

Retrofit the Classroom class with an interface

// 声明一个接口
interface CanPlay {
    
    
	name: string;
	play(): void;
}

class Classroom {
    
    
	// 当函数的参数类型不需要确定为某个具体的类,而是要限制其拥有哪些属性和方法,就可以用接口进行限制
	play(who: CanPlay): void {
    
    
		who.play();
	}
}

// 声明类的时候可以用接口进行限制,声明这个类实现某个接口,那么这个类中就必须实现接口中声明的属性和方法,否则会报错
class Child implements CanPlay {
    
    
	name: string;
	constructor(name: string) {
    
    
		this.name = name;
	}
	play(): void {
    
    
		console.log(this.name + '正在玩耍');
	}
}

class Student implements CanPlay {
    
    
	name: string;
	constructor(name: string) {
    
    
		this.name = name;
	}
	play(): void {
    
    
		console.log(this.name + '正在玩耍');
	}
}

let child: Child = new Child('小明');
let stu: Student = new Student('小刚');
let room: Classroom = new Classroom();

room.play(child);
room.play(stu);

// 在方法中也可以使用接口限制参数的类型,只要是实现了CanPlay接口类的实例都可以传递进来
function wan(obj: CanPlay): void {
    
    
	obj.play();
}
wan(child);
wan(stu);

Difference between interface and type alias

In fact, in most cases, the effects of using interface types and type aliases are equivalent, but there are still big differences between the two in some specific scenarios.

One of the core principles of TypeScript is type checking the structure a value has. The role of the interface is to name these types and define the data model for your code or third-party code.

type (type alias) gives a new name to a type. type is sometimes similar to interface, but can be applied to primitive values ​​(primitive types), union types, tuples, and any other type you need to write by hand. Aliasing doesn't create a new type - it creates a new name to refer to that type. Aliasing primitive types is generally not useful, although it can be used as a form of documentation

Objects / Functions

Both can be used to describe the type of an object or function, but the syntax is different.

Interface

interface Point {
    
    
  x: number;
  y: number;
}

interface SetPoint {
    
    
  (x: number, y: number): void;
}

Type alias

type Point = {
    
    
  x: number;
  y: number;
};

type SetPoint = (x: number, y: number) => void;

Other Types

Unlike interfaces, type aliases can also be used for other types such as primitive types (primitive values), union types, tuples.

// primitive
type Name = string;

// object
type PartialPointX = {
    
     x: number; };
type PartialPointY = {
    
     y: number; };

// union
type PartialPoint = PartialPointX | PartialPointY;

// tuple
type Data = [number, string];

// dom
let div = document.createElement('div');
type B = typeof div;

Interfaces can be defined multiple times, type aliases cannot

Unlike type aliases, interfaces can be defined multiple times and are automatically merged into a single interface.

interface Point {
    
     x: number; }
interface Point {
    
     y: number; }
const point: Point = {
    
     x: 1, y: 2 };

expand

The two scale differently, but are not mutually exclusive. Interfaces can extend type aliases, and type aliases can also extend interfaces.

The extension of the interface is inheritance, which is realized extendsthrough . The extension of type alias is cross type, which is realized &by .

Interface extension interface

interface PointX {
    
    
    x: number
}

interface Point extends PointX {
    
    
    y: number
}

type aliases extend type aliases

type PointX = {
    
    
    x: number
}

type Point = PointX & {
    
    
    y: number
}

Interface extension type alias

type PointX = {
    
    
    x: number
}
interface Point extends PointX {
    
    
    y: number
}

type alias extension interface

interface PointX {
    
    
    x: number
}
type Point = PointX & {
    
    
    y: number
}

generic

Introduction to Generics

If you were asked to implement a function identitywhose parameter could be any value, and the return value was to return the parameter as it is, and it could only accept one parameter, what would you do?

You will think this is very simple, just write the code like this:

const identity = (arg) => arg;

Since it can accept any value, that is to say, the input parameter and return value of your function should be of any type. Now let's add type declarations to the code:

type idBoolean = (arg: boolean) => boolean;
type idNumber = (arg: number) => number;
type idString = (arg: string) => string;
...

A stupid method is like the above, that is to say, as many types as JS provides, you need to copy as many copies of the code, and then change the type signature. This is fatal for programmers. This kind of copying and pasting increases the probability of errors, makes the code difficult to maintain, and affects the whole body. And if new types are added to JS in the future, you still need to modify the code, which means that your code is open to modification , which is not good. Another way is to use the "universal syntax" of any. What are the disadvantages? Let me give you an example:

identity("string").length; // ok
identity("string").toFixed(2); // ok
identity(null).toString(); // ok
...

If you use any, whatever you write is ok, which loses the effect of type checking. In fact, I know that what I passed to you is a string, and the return must be a string, and there is no toFixed method on the string, so the need to report an error is what I want. In other words, the effect I really want is: 当我用到id的时候,你根据我传给你的类型进行推导. For example, if I pass in a string, but use the method on number, you should report an error.

In order to solve the above problems, we use generics to refactor the above code . Different from our definition, a type T is used here, this T is an abstract type, and its value is only determined when it is called , so we don't need to copy and paste countless copies of code.

function identity<T>(arg: T): T {
    
    
  return arg;
}

where Tstands for Type and is usually used as the first type variable name when defining generics. But virtually any valid name Tcan be substituted. In Taddition to , the following are the meanings of common generic variables:

  • K (Key): Indicates the key type in the object;
  • V (Value): Indicates the value type in the object;
  • E (Element): Indicates the element type.

Here is a picture to help you understandimage.png

In fact, we can not only define one type variable, we can introduce any number of type variables we want to define. For example, we introduce a new type variable Uto extend identitythe function :

function identity <T, U>(value: T, message: U) : T {
    
    
  console.log(message);
  return value;
}
console.log(identity<Number, string>(68, "Semlinker"));

image.png

Instead of explicitly setting values ​​for type variables, it is more common to have the compiler choose these types automatically, resulting in cleaner code. We can omit the angle brackets entirely, like:

function identity <T, U>(value: T, message: U) : T {
    
    
  console.log(message);
  return value;
}
console.log(identity(68, "Semlinker"));

For the above code, the compiler is smart enough to know the types of our parameters and assign them to T and U without the developer needing to specify them explicitly.

Generic constraints

What if I want to print out the size attribute of the parameter? If TS is not constrained at all, an error will be reported:

function trace<T>(arg: T): T {
    
    
  console.log(arg.size); // Error: Property 'size doesn't exist on type 'T'
  return arg;
}

The reason for the error is that T can be of any type in theory, unlike any, you will report an error no matter what property or method you use (unless this property and method are common to all collections). Then the intuitive idea is to limit the parameter type passed to the trace function to the size type, so that no error will be reported. How to express the point of this type constraint ? The key to achieving this requirement is to use type constraints. This can be done using the extends keyword. Simply put, you define a type, and then let T implement this interface.

interface Sizeable {
    
    
  size: number;
}
function trace<T extends Sizeable>(arg: T): T {
    
    
  console.log(arg.size);
  return arg;
}

Some people may say that I can directly limit the parameters of Trace to Sizeable type? If you do this, there is a risk of type loss, please refer to this article A use case for TypeScript Generics for details .

generic utility type

For the convenience of developers, TypeScript has built-in some commonly used tool types, such as Partial, Required, Readonly, Record, and ReturnType. But before the specific introduction, we have to introduce some relevant basic knowledge first, so that readers can better learn other tool types.

1.typeof

The main purpose of typeof is to obtain the type of a variable or property in a type context. Let's understand it through a specific example.

interface Person {
    
    
  name: string;
  age: number;
}
const sem: Person = {
    
     name: "semlinker", age: 30 };
type Sem = typeof sem; // type Sem = Person

In the above code, we get the type of the sem variable through typeofthe operator and assign it to the Sem type variable, then we can use the Sem type:

const lolo: Sem = {
    
     name: "lolo", age: 5 }

You can also do the same with nested objects:

const Message = {
    
    
    name: "jimmy",
    age: 18,
    address: {
    
    
      province: '四川',
      city: '成都'   
    }
}
type message = typeof Message;
/*
 type message = {
    name: string;
    age: number;
    address: {
        province: string;
        city: string;
    };
}
*/

In addition, typeofin addition to obtaining the structural type of the object, the operator can also be used to obtain the type of the function object, such as:

function toArray(x: number): Array<number> {
    
    
  return [x];
}
type Func = typeof toArray; // -> (x: number) => number[]

2.keyof

keyofThe operator was introduced in TypeScript 2.1. This operator can be used to get all the keys of a certain type, and its return type is a union type.

interface Person {
    
    
  name: string;
  age: number;
}

type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join" 
type K3 = keyof {
    
     [x: string]: Person };  // string | number

Two types of index signatures are supported in TypeScript, numeric indexes and string indexes:

interface StringArray {
    
    
  // 字符串索引 -> keyof StringArray => string | number
  [index: string]: string; 
}

interface StringArray1 {
    
    
  // 数字索引 -> keyof StringArray1 => number
  [index: number]: string;
}

In order to support both index types at the same time, it is required that the return value of the numeric index must be a subclass of the return value of the string index. The reason is that when using a numeric index, JavaScript will first convert the numeric index to a string index when performing an index operation . So the result keyof { [x: string]: Person }of will be returned string | number.

keyof also supports basic data types:

let K1: keyof boolean; // let K1: "valueOf"
let K2: keyof number; // let K2: "toString" | "toFixed" | "toExponential" | ...
let K3: keyof symbol; // let K1: "valueOf"

The role of keyof

JavaScript is a highly dynamic language. Sometimes it can be tricky to capture the semantics of certain operations in a static type system. Take a simple propfunction as an example:

function prop(obj, key) {
    
    
  return obj[key];
}

This function receives two parameters, obj and key, and returns the value of the corresponding property. Different properties on the object can have completely different types, and we don't even know what the obj object looks like.

So how to define the above propfunction ? Let's try it out:

function prop(obj: object, key: string) {
    
    
  return obj[key];
}

In the above code, in order to avoid passing in the wrong parameter type when calling the prop function, we set the types for the obj and key parameters, which are {}and stringtypes respectively. However, things are not that simple. For the above code, the TypeScript compiler will output the following error message:

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{
    
    }'.

Elements implicitly have anythe type because stringthe type cannot be used to index {}the type . To fix this, you can use the following very brute-force solution:

function prop(obj: object, key: string) {
    
    
  return (obj as any)[key];
}

Obviously, this solution is not a good solution. Let's review the function of propthe function , which is used to obtain the attribute value of a specified attribute in an object. Therefore, we expect that the attribute entered by the user is an attribute that already exists on the object, so how to limit the scope of the attribute name? At this time, we can use the protagonist keyofoperator :

function prop<T extends object, K extends keyof T>(obj: T, key: K) {
    
    
  return obj[key];
}

In the above code, we have used TypeScript's generics and generic constraints. First define the T type and use extendsthe keyword constraint that the type must be a subtype of the object type, then use keyofthe operator to get all the keys of the T type, and its return type is a union type, and finally use extendsthe keyword constraint that the K type must be keyof Ta union type Subtype. You will know if it is a mule or a horse for a walk, let’s test it out:

type Todo = {
    
    
  id: number;
  text: string;
  done: boolean;
}

const todo: Todo = {
    
    
  id: 1,
  text: "Learn TypeScript keyof",
  done: false
}

function prop<T extends object, K extends keyof T>(obj: T, key: K) {
    
    
  return obj[key];
}

const id = prop(todo, "id"); // const id: number
const text = prop(todo, "text"); // const text: string
const done = prop(todo, "done"); // const done: boolean

Obviously, using generics, the redefined prop<T extends object, K extends keyof T>(obj: T, key: K)function can already correctly deduce the type corresponding to the specified key. So what happens when you access a property that doesn't exist on the todo object? for example:

const date = prop(todo, "date");

For the above code, the TypeScript compiler will prompt the following error:

Argument of type '"date"' is not assignable to parameter of type '"id" | "text" | "done"'.

This prevents us from trying to read properties that don't exist.

3.in

inUsed to traverse enumerated types:

type Keys = "a" | "b" | "c"

type Obj =  {
    
    
  [p in Keys]: any
} // -> { a: any, b: any, c: any }

4.infer

In a conditional type statement, you can inferdeclare a type variable and use it.

type ReturnType<T> = T extends (
  ...args: any[]
) => infer R ? R : any;

In the above code, a variable infer Ris declared to carry the return value type of the incoming function signature. Simply put, it is used to obtain the type of the function return value for later use.

5.extends

Sometimes the generics we define do not want to be too flexible or want to inherit certain classes, etc., we can add generic constraints through the extends keyword.

interface Lengthwise {
    
    
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    
    
  console.log(arg.length);
  return arg;
}

Now the generic function is constrained so it no longer applies to any type:

loggingIdentity(3);  // Error, number doesn't have a .length property

At this time, we need to pass in a value that conforms to the constraint type, which must contain the length attribute:

loggingIdentity({
    
    length: 10, value: 3});

index type

In actual development, we often encounter such a scenario, get the value of some attributes in the object, and then create the corresponding collection.

let person = {
    
    
    name: 'musion',
    age: 35
}

function getValues(person: any, keys: string[]) {
    
    
    return keys.map(key => person[key])
}

console.log(getValues(person, ['name', 'age'])) // ['musion', 35]
console.log(getValues(person, ['gender'])) // [undefined]

In the above example, you can see that getValues(persion, ['gender']) prints [undefined], but the ts compiler does not give an error message, so how to use ts to constrain the type of this mode? The index type will be used here, modify the getValues ​​function, and use the index type query and index access operators:

function getValues<T, K extends keyof T>(person: T, keys: K[]): T[K][] {
    
    
  return keys.map(key => person[key]);
}

interface Person {
    
    
    name: string;
    age: number;
}

const person: Person = {
    
    
    name: 'musion',
    age: 35
}

getValues(person, ['name']) // ['musion']
getValues(person, ['gender']) // 报错:
// Argument of Type '"gender"[]' is not assignable to parameter of type '("name" | "age")[]'.
// Type "gender" is not assignable to type "name" | "age".


The compiler checks that the value passed in is part of Person. The above code can be understood by the following concepts:

T[K]表示对象T的属性K所表示的类型,在上述例子中,T[K][] 表示变量T取属性K的值的数组

// 通过[]索引类型访问操作符, 我们就能得到某个索引的类型
class Person {
    
    
    name:string;
    age:number;
 }
 type MyType = Person['name'];  //Person中name的类型为string type MyType = string


After introducing the concept, you should be able to understand the above code. First look at generics. There are two types, T and K. According to type inference, the first parameter person is person, and the type will be inferred as Person. As for the type inference of the second array parameter (K ​​extends keyof T), the keyof keyword can obtain T, which is all attribute names of Person, namely ['name', 'age']. The extends keyword allows the generic type K to inherit all attribute names of Person, namely ['name', 'age']. The combination of these three features ensures the dynamics and accuracy of the code, and also makes the code hints richer

getValues(person, ['gender']) // 报错:
// Argument of Type '"gender"[]' is not assignable to parameter of type '("name" | "age")[]'.
// Type "gender" is not assignable to type "name" | "age".

mapping type

Create a new type based on the old type, we call it a mapped type

For example, we define an interface

interface TestInterface{
    
    
    name:string,
    age:number
}

We make all the properties in the interface defined above optional

// 我们可以通过+/-来指定添加还是删除

type OptionalTestInterface<T> = {
    
    
  [p in keyof T]+?:T[p]
}

type newTestInterface = OptionalTestInterface<TestInterface>
// type newTestInterface = {
    
    
//    name?:string,
//    age?:number
// }

For example, we add read-only

type OptionalTestInterface<T> = {
    
    
 +readonly [p in keyof T]+?:T[p]
}

type newTestInterface = OptionalTestInterface<TestInterface>
// type newTestInterface = {
    
    
//   readonly name?:string,
//   readonly age?:number
// }

Since it is common to generate read-only attributes and optional attributes, TS has provided us with a ready-made implementation of Readonly / Partial, and the built-in tool types will be introduced.

Built-in tool types

Partial

Partial<T>Make a type property optional

definition

type Partial<T> = {
    
    
  [P in keyof T]?: T[P];
};

In the above code, first keyof Tget Tall the attribute names of , then use into traverse, assign the value to P, and finally T[P]get . A middle ?sign is used to make all attributes optional.

for example

interface UserInfo {
    
    
    id: string;
    name: string;
}
// error:Property 'id' is missing in type '{ name: string; }' but required in type 'UserInfo'
const xiaoming: UserInfo = {
    
    
    name: 'xiaoming'
}

usePartial<T>

type NewUserInfo = Partial<UserInfo>;
const xiaoming: NewUserInfo = {
    
    
    name: 'xiaoming'
}

This NewUserInfo is equivalent to

interface NewUserInfo {
    
    
    id?: string;
    name?: string;
}

But Partial<T>there is a limitation, that is, it only supports processing the attributes of the first layer, if my interface definition is like this

interface UserInfo {
    
    
    id: string;
    name: string;
    fruits: {
    
    
        appleNumber: number;
        orangeNumber: number;
    }
}

type NewUserInfo = Partial<UserInfo>;

// Property 'appleNumber' is missing in type '{ orangeNumber: number; }' but required in type '{ appleNumber: number; orangeNumber: number; }'.
const xiaoming: NewUserInfo = {
    
    
    name: 'xiaoming',
    fruits: {
    
    
        orangeNumber: 1,
    }
}

It can be seen that after the second layer, it will not be processed. If you want to process multiple layers, you can implement it yourself

DeepPartial

type DeepPartial<T> = {
    
    
     // 如果是 object,则递归类型
    [U in keyof T]?: T[U] extends object
      ? DeepPartial<T[U]>
      : T[U]
};

type PartialedWindow = DeepPartial<T>; // 现在T上所有属性都变成了可选啦

Required

Required turns the attributes of the type into mandatory

definition

type Required<T> = {
    
     
    [P in keyof T]-?: T[P] 
};

Where -?is ?the flag that represents removing this modifier. To expand, in addition to being applied to ?this modifiers, there are also applications readonly, such as Readonly<T>this type

type Readonly<T> = {
    
    
    readonly [p in keyof T]: T[p];
}

Readonly

Readonly<T>The function of is to turn all properties of a certain type into read-only properties, which means that these properties cannot be reassigned.

definition

type Readonly<T> = {
    
    
 readonly [P in keyof T]: T[P];
};

for example

interface Todo {
    
    
 title: string;
}

const todo: Readonly<Todo> = {
    
    
 title: "Delete inactive users"
};

todo.title = "Hello"; // Error: cannot reassign a readonly property

Pick

Pick picks out some attributes from a type

definition

type Pick<T, K extends keyof T> = {
    
    
    [P in K]: T[P];
};

for example

interface Todo {
    
    
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Pick<Todo, "title" | "completed">;

const todo: TodoPreview = {
    
    
  title: "Clean room",
  completed: false,
};

You can see that there is only one name attribute in NewUserInfo.

Record

Record<K extends keyof any, T>The role of is to convert the values ​​of all attributes Kin to Tthe type.

definition

type Record<K extends keyof any, T> = {
    
    
    [P in K]: T;
};

for example

interface PageInfo {
    
    
  title: string;
}

type Page = "home" | "about" | "contact";

const x: Record<Page, PageInfo> = {
    
    
  about: {
    
     title: "about" },
  contact: {
    
     title: "contact" },
  home: {
    
     title: "home" },
};

ReturnType

Used to get the return type of a function

definition

type ReturnType<T extends (...args: any[]) => any> = T extends (
  ...args: any[]
) => infer R
  ? R
  : any;

inferUsed here to extract the return type of the function type. ReturnType<T>Just move infer R from the parameter position to the return value position, so at this time R represents the return value type to be inferred.

for example

type Func = (value: number) => string;
const foo: ReturnType<Func> = "1";

ReturnTypeFuncThe return value type obtained is string, so fooit can only be assigned as a string.

Exclude

Exclude<T, U>The function is to remove the type belonging to another type in one type.

definition

type Exclude<T, U> = T extends U ? never : T;

If Tis assignable to Uthe type, then neverthe type , otherwise Tthe type is returned. The final effect is Tto Uremove some of the types belonging to .

for example

type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
type T2 = Exclude<string | number | (() => void), Function>; // string | number

Extract

Extract<T, U>The role of is to extract Tfrom U.

definition

type Extract<T, U> = T extends U ? T : never;

for example

type T0 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
type T1 = Extract<string | number | (() => void), Function>; // () =>void

Omit

Omit<T, K extends keyof any>The function of Tis Kto construct a new type using all attributes of the type except the type.

definition

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

for example

interface Todo {
    
    
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Omit<Todo, "description">;

const todo: TodoPreview = {
    
    
  title: "Clean room",
  completed: false,
};

NonNullable

NonNullable<T>The function of is to filter the nulland undefined.

definition

type NonNullable<T> = T extendsnull | undefined ? never : T;

for example

type T0 = NonNullable<string | number | undefined>; // string | number
type T1 = NonNullable<string[] | null | undefined>; // string[]

Parameters

Parameters<T>The function is to obtain the tuple type composed of the parameter types of the function.

definition

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any
? P : never;

for example

type A = Parameters<() =>void>; // []
type B = Parameters<typeofArray.isArray>; // [any]
type C = Parameters<typeofparseInt>; // [string, (number | undefined)?]
type D = Parameters<typeofMath.max>; // number[]

Some tips for writing efficient TS code

minimize duplication of code

For those who are new to TypeScript, when defining an interface, the following similar repetitive code may accidentally appear. for example:

interface Person {
    
    
  firstName: string;
  lastName: string;
}

interface PersonWithBirthDate {
    
    
  firstName: string;
  lastName: string;
  birth: Date;
}

Obviously, compared to Personthe interface , PersonWithBirthDatethe interface just has one more birthattribute , and the other attributes Personare the same as the interface. So how to avoid the duplication of code in the example? To solve this problem, you can take advantage of extendsthe keyword :

interface Person {
    
     
  firstName: string; 
  lastName: string;
}

interface PersonWithBirthDate extends Person {
    
     
  birth: Date;
}

Of course, in addition to using extendsthe keyword , you can also use the intersection operator (&):

type PersonWithBirthDate = Person & {
    
     birth: Date };

Additionally, sometimes you may find yourself wanting to define a type to match the "shape" of an initial configuration object, such as:

const INIT_OPTIONS = {
    
    
  width: 640,
  height: 480,
  color: "#00FF00",
  label: "VGA",
};

interface Options {
    
    
  width: number;
  height: number;
  color: string;
  label: string;
}

In fact, for the Options interface, you can also use the typeof operator to quickly obtain the "shape" of the configuration object:

type Options = typeof INIT_OPTIONS;

In actual development, duplicate types are not always easy to spot. Sometimes they are masked by syntax. For example, there are multiple functions with the same type signature:

function get(url: string, opts: Options): Promise<Response> {
    
     /* ... */ } 
function post(url: string, opts: Options): Promise<Response> {
    
     /* ... */ }

For the above get and post methods, in order to avoid duplication of code, you can extract a unified type signature:

type HTTPFunction = (url: string, opts: Options) => Promise<Response>; 
const get: HTTPFunction = (url, opts) => {
    
     /* ... */ };
const post: HTTPFunction = (url, opts) => {
    
     /* ... */ };

Use a more precise type instead of a string type

Say you're building a music collection and want to define a genre for albums. At this point you can use interfacethe keyword to define a Albumtype:

interface Album {
    
    
  artist: string; // 艺术家
  title: string; // 专辑标题
  releaseDate: string; // 发行日期:YYYY-MM-DD
  recordingType: string; // 录制类型:"live" 或 "studio"
}

For Albumthe type , you want releaseDatethe attribute value to be in the format of YYYY-MM-DDand the range of recordingTypethe attribute value to be liveor studio. But releaseDatebecause recordingTypethe types of the and attributes in the interface are both strings, the following problems may occur when using Albumthe interface :

const dangerous: Album = {
    
    
  artist: "Michael Jackson",
  title: "Dangerous",
  releaseDate: "November 31, 1991", // 与预期格式不匹配
  recordingType: "Studio", // 与预期格式不匹配
};

releaseDateAlthough recordingTypethe values ​​of and don't match the expected format, the TypeScript compiler doesn't spot the problem at this point. To fix this, you releaseDateshould recordingTypedefine more precise types for the and properties, like this:

interface Album {
    
    \
  artist: string; // 艺术家
  title: string; // 专辑标题
  releaseDate: Date; // 发行日期:YYYY-MM-DD
  recordingType: "studio" | "live"; // 录制类型:"live" 或 "studio"
}

After redefining Albumthe interface , the TypeScript compiler will prompt the following exception information for the previous assignment statement:

const dangerous: Album = {
    
    
  artist: "Michael Jackson",
  title: "Dangerous",
  // 不能将类型“string”分配给类型“Date”。ts(2322)
  releaseDate: "November 31, 1991", // Error
  // 不能将类型“"Studio"”分配给类型“"studio" | "live"”。ts(2322)\
  recordingType: "Studio", // Error
};

In order to solve the above problem, you need releaseDateto recordingTypeset the correct type for the and properties, like this:

const dangerous: Album = {
    
    
  artist: "Michael Jackson",
  title: "Dangerous",
  releaseDate: new Date("1991-11-31"),
  recordingType: "studio",
};

A defined type always represents a valid state

Suppose you're building a web application that allows the user to specify a page number, then loads and displays the corresponding content for that page. First, you might define Statethe object :

interface State {
    
    
  pageContent: string;
  isLoading: boolean;
  errorMsg?: string;
}

Then you define a renderPagefunction to render the page:

function renderPage(state: State) {
    
    
  if (state.errorMsg) {
    
    
    return `呜呜呜,加载页面出现异常了...${
      
      state.errorMsg}`;
  } else if (state.isLoading) {
    
    
    return `页面加载中~~~`;
  }
  return `<div>${
      
      state.pageContent}</div>`;
}

// 输出结果:页面加载中~~~
console.log(renderPage({
    
    isLoading: true, pageContent: ""}));
// 输出结果:<div>大家好</div>
console.log(renderPage({
    
    isLoading: false, pageContent: "大家好呀"}));

After creating renderPagethe function , you can continue to define a changePagefunction to get the corresponding page data according to the page number:

async function changePage(state: State, newPage: string) {
    
    
  state.isLoading = true;
  try {
    
    
    const response = await fetch(getUrlForPage(newPage));
    if (!response.ok) {
    
    
      throw new Error(`Unable to load ${
      
      newPage}: ${
      
      response.statusText}`);
    }
    const text = await response.text();
    state.isLoading = false;
    state.pageContent = text;
  } catch (e) {
    
    
    state.errorMsg = "" + e;
  }
}

For the above changePagefunction , it has the following problems:

  • In the catch statement, the state state.isLoadingof set to false;
  • state.errorMsgThe value of was not cleaned up in time , so if a previous request failed, you'll continue to see error messages instead of loading messages.

The above problem occurs because the previously defined Statetype allows the values ​​ofisLoading and to be set at the same time, although this is an invalid state. errorMsgTo solve this problem, you can consider introducing a recognized union type to define different page request states:

interface RequestPending {
    
    
  state: "pending";
}

interface RequestError {
    
    
  state: "error";
  errorMsg: string;
}

interface RequestSuccess {
    
    
  state: "ok";
  pageContent: string;
}

type RequestState = RequestPending | RequestError | RequestSuccess;

interface State {
    
    
  currentPage: string;
  requests: {
    
     [page: string]: RequestState };
}

In the above code, 3 different request states are defined by using identifiable joint types, so that different request states can be easily distinguished, thus making the business logic processing clearer. Next, you need to update the previously created and functions based on the updated Statetype :renderPagechangePage

Updated renderPage function

function renderPage(state: State) {
    
    
  const {
    
     currentPage } = state;
  const requestState = state.requests[currentPage];
  switch (requestState.state) {
    
    
    case "pending":
      return `页面加载中~~~`;
    case "error":
      return `呜呜呜,加载第${
      
      currentPage}页出现异常了...${
      
      requestState.errorMsg}`;
    case "ok":
      `<div>第${
      
      currentPage}页的内容:${
      
      requestState.pageContent}</div>`;
  }
}

Updated changePage function

async function changePage(state: State, newPage: string) {
    
    
  state.requests[newPage] = {
    
     state: "pending" };
  state.currentPage = newPage;
  try {
    
    
    const response = await fetch(getUrlForPage(newPage));
    if (!response.ok) {
    
    
      throw new Error(`无法正常加载页面 ${
      
      newPage}: ${
      
      response.statusText}`);
    }
    const pageContent = await response.text();
    state.requests[newPage] = {
    
     state: "ok", pageContent };
  } catch (e) {
    
    
    state.requests[newPage] = {
    
     state: "error", errorMsg: "" + e };
  }
}

In changePagethe function , different request states will be set according to different situations, and different request states will contain different information. In this way, renderPagethe function can stateperform corresponding processing according to the unified attribute value. Therefore, by using the Discriminated Union type, each state of the request is a valid state, and there will be no problem of invalid state.

More

Just talk but don't practice fake moves, click the link below to practice your hands

Guess you like

Origin blog.csdn.net/weixin_68658847/article/details/130501204