Author: a tuque warehouse: Github, Gitee Tuque community main station (first) : Tuque community blog: Digijin, Zhihu, MOC public number: Tuque community contact me: after paying attention to the public number can add Tuque sauce wechat oh original is not easy, ❤️ like + comment + collection ❤️ three, encourage the author to write a better tutorial.

Welcome to TypeScript’s Getting Started to Practice series on Type is Justice:

  • Type is Justice: TypeScript from Starter to Practice (Prologue)
  • Type is Justice: TypeScript from Inception to Practice (PART 1)
  • Type is Justice: TypeScript from Getting Started to Practice, PART II

To understand the basic types of TS interface, we began to learn how to give more complex structure annotation type, this is we want to draw out this section function, and then we explain how to operation type: cross type and joint type, finally, we explained the atom types: literal type, and how to implement type and combined type collocation guard effect.

The source code for this article is available on Github or Gitee. If you think it is well written, please give ❤️ a thumbs up and Github or Gitee a star ❤️

This tutorial is part of the React front End engineer’s learning path. Welcome to Star and encourage us to continue to create better tutorials

function

We annotated the onChange function in TodoInputProps earlier, but we didn’t cover it in detail, so in this section we’ll look at the TS function in more detail.

Annotation function

For example, we have the following function:

function add(x, y) {
  return x + y;
}
Copy the code

So how do we annotate this function? In fact, the main part of a function is the input and output, so when we annotate a function, we only need to annotate the parameter and return value of the function, because the above function is to perform x+y operation, x and y should be number, and the return value should be number. So we annotate the above function as follows:

function add(x: number, y: number) :number {
  return x + y;
}
Copy the code

You can see that we annotate x and y with the number type as colon annotations, and for the return value, we annotate it directly as add(): number. Sometimes the return value is not written. TS can calculate the return value type based on the parameter type and function body, which is commonly known as the automatic type inference mechanism.

Function types

In addition to annotating functions, we sometimes involve assigning functions to a variable, as in the following example:

const add = function (x, y) {
  return x + y;
}
Copy the code

A function type is args1: type1, args2: type2, args2: type2… Args2: typen) => the type of returnType, so we can comment the above example as follows:

const add: (x: number, y: number) :number =  function(x, y) {
  return x + y;
}
Copy the code

If you have any questions, we annotated the function type of the add variable, but we didn’t annotate the function behind it. In fact, TS will automatically deduce the type. By comparing the structure of the function type with that of the following function, it will automatically infer that the x, y and return value of the latter function are all number.

Optional parameters

Just as there are optional attributes in the Interface before, there are also optional parameters in our function, because one of the biggest benefits of using TS is to make the type definition of function and Interface as clear as possible, so that other team members can clearly understand the Interface of the code and greatly improve the efficiency of team collaboration. So if a function might have some parameters, but we don’t need to pass them every time, they fall into the optional parameters category.

For example, if we want to write a function that constructs a person’s name, including firstName and lastName, but sometimes we don’t know lastName, how to write such a function? :

function buildName(firstName: string, lastName? :string) {
  // ...
}
Copy the code

As you can see above, we have to pass firstName to build a person’s name function, but since lastName may not be available at times, we set it as an optional parameter, so the following function calls are available:

buildName('Tom'.'Huang');
buildName('mRcfps');
Copy the code

overloading

Overloading (Overloads) is a special characteristic in TS, in JS: no, it is mainly for the function return type service, more specifically a function may be performed inside a conditional statement that returns the value of different according to different conditions, these values may be of different types, so this time we how to give the return value annotation type?

The answer is to use overloading to annotate a multi-type return value function by defining a series of functions with the same function name, different argument lists, and return values. Let’s look at a multi-type return function:

let suits = ["hearts"."spades"."clubs"."diamonds"];

function pickCard(x) :any {
  // If x is of type 'object', then we return pickCard and fetch pickCard D1 data from myDeck
  if (typeof x == "object") {
    let pickedCard = Math.floor(Math.random() * x.length);
    return pickedCard;
  }
  // If x is of type 'number', return a pickCard2 that can fetch data directly
  else if (typeof x == "number") {
    let pickedSuit = Math.floor(x / 13);
    return { suit: suits[pickedSuit], card: x % 13}; }}let myDeck = [
  { suit: "diamonds", card: 2 },
  { suit: "spades", card: 10 },
  { suit: "hearts", card: 4}];let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);

let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
Copy the code

In this example, the pickCard function has a different return type depending on the type of x. In this example, the pickCard function has a different return type depending on the type of x. Yes, in the previous example, the parameter type had only one option, so the return value type could be automatically inferred, but here the situation is: “The parameter type can have multiple options, corresponding to the parameter type of different options, there will be different return value types, and we do not know the parameter type”. In this case, we decouple the correspondence directly, which is best expressed using overloading:

let suits = ["hearts"."spades"."clubs"."diamonds"];

function pickCard(x: { suit: string; card: number} []) :number;
function pickCard(x: number) :{ suit: string; card: number };
function pickCard(x) :any {
  // If x is of type 'object', then we return pickCard and fetch pickCard D1 data from myDeck
  if (typeof x == "object") {
    let pickedCard = Math.floor(Math.random() * x.length);
    return pickedCard;
  }
  // If x is of type 'number', return a pickCard2 that can fetch data directly
  else if (typeof x == "number") {
    let pickedSuit = Math.floor(x / 13);
    return { suit: suits[pickedSuit], card: x % 13}; }}let myDeck = [
  { suit: "diamonds", card: 2 },
  { suit: "spades", card: 10 },
  { suit: "hearts", card: 4}];let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);

let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
Copy the code

Function pickCard(x: type1): type2 function pickCard(x: type1): type2 function pickCard(x: type1): type2

  • The first overload, we give argumentsxAssigns an array type whose item is an object containing two propertiessuitcardAnd their types are respectivelystringnumber; It then returns a value of typenumberType, this corresponds toxThe type ofobject, the return type isnumberThis is the case.
  • The second overload, we give argumentsxI assign onenumberType and then return value Type is an object that has two propertiessuitcard, the corresponding type isstringnumber; The correspondingxThe type ofnumberThe return value type isobjectType in this case.

Hands-on practice

SRC /utils/data. TS SRC /utils/data. TS

export interface Todo {
  id: string;
  user: string;
  date: string;
  content: string;
  isCompleted: boolean;
}

export interface User {
  id: string;
  name: string;
  avatar: string;
}

export function getUserById(userId: string) {
  return userList.filter(user= > user.id === userId)[0];
}

export const todoListData: Todo[] = [
  {
    id: "1",
    content: "Tuq Community: A Great Free Tutorial.",
    user: "23410977",
    date: "19:34 on 2 March 2020",
    isCompleted: false
  },
  {
    id: "2",
    content: "Tuq Community: A Great Free Tutorial.",
    user: "23410976",
    date: "19:34 on 2 March 2020",
    isCompleted: false
  },
  {
    id: "3",
    content: "Tuq Community: A Great Free Tutorial.",
    user: "58352313",
    date: "19:34 on 2 March 2020",
    isCompleted: false
  },
  {
    id: "4",
    content: "Tuq Community: A Great Free Tutorial.",
    user: "25455350",
    date: "19:34 on 2 March 2020",
    isCompleted: false
  },
  {
    id: "5",
    content: "Tuq Community: A Great Free Tutorial.",
    user: "12345678",
    date: "19:34 on 2 March 2020",
    isCompleted: true}];export const userList: User[] = [
  // ...
  {
    id: "23410976",
    name: "pftom",
    avatar: "https://avatars1.githubusercontent.com/u/26423749?s=88&v=4"
  },
  // ...
  {
    id: "12345678",
    name: "pony",
    avatar: "https://avatars3.githubusercontent.com/u/25010151?s=96&v=4"}];Copy the code

As you can see, we mainly made the following changes above:

  • willtodoListDataOf each element ofuserField changed to corresponduserListElements of theid, convenient based onuseridSearch for user information.
  • And then we givetodoListDataEach element is addedidConvenient sign, then puttimeProperty is replaced bydateProperties.
  • And then we defined onegetUserByIdFunction for eachtodoAccording to theuser< span style = “max-width: 100%; clear: both; min-height: 100%; This is also a case where TS automatic type inference is used. The TS compiler calculates the return value type based on the parameter type, so we don’t need to specify the return value explicitly.
  • And then finally we derived itTodoUserInterface.

TSX: SRC /TodoList. TSX: SRC /App. TSX: SRC /TodoList. TSX: SRC /App.

import React from "react"; import { List, Avatar, Menu, Dropdown } from "antd"; import { DownOutlined } from "@ant-design/icons"; import { Todo, getUserById } from "./utils/data"; Const menu = (< menu > < menu. Item> Complete </ menu. Item> < menu. Item> delete </ menu. Item> </ menu >); interface TodoListProps { todoList: Todo[]; } function TodoList({ todoList }: TodoListProps) { return ( <List className="demo-loadmore-list" itemLayout="horizontal" dataSource={todoList} renderItem={item => { const user = getUserById(item.user); Return (< list. Item key={item.id} actions={[<Dropdown overlay={menu}> <a key=" list-loader-more "> action <DownOutlined /> </a> </Dropdown> ]} > <List.Item.Meta avatar={<Avatar src={user.avatar} />} title={<a href="https://ant.design">{user.name}</a>} description={item.date} /> <div style={{ textDecoration: item.isCompleted ? "line-through" : "none" }} > {item.content} </div> </List.Item> ); }} / >); } export default TodoList;Copy the code

As you can see, the main changes we made above are as follows:

  • We imported it firstTodoInterface added to TodoList componentTodoListPropsInterface for this componentpropsMake type annotations.
  • And then we import the sumgetUserByIdforrenderItemAccording to the insideitem.userGet the details of the user, then display the avatar and name.
  • And then we’re going toitem.timeUpdated toitem.date
  • Finally, we set up based on whether the to-do list has been completedline-throughtextDecorationProperty to mark completed items.

SRC/app.tsx SRC/app.tsx

import React, { useRef, useState } from "react"; import { List, Avatar, // ... Dropdown, Tabs } from "antd"; import TodoInput from "./TodoInput"; import TodoList from "./TodoList"; import { todoListData } from "./utils/data"; import "./App.css"; import logo from "./logo.svg"; const { Title } = Typography; const { TabPane } = Tabs; function App() { const [todoList, setTodoList] = useState(todoListData); const callback = () => {}; const onFinish = (values: any) => { const newTodo = { ... values.todo, isCompleted: false }; setTodoList(todoList.concat(newTodo)); }; const ref = useRef(null); const activeTodoList = todoList.filter(todo => ! todo.isCompleted); const completedTodoList = todoList.filter(todo => todo.isCompleted); return ( <div className="App" ref={ref}> <div className="container header"> // ... <div className="container"> <Tabs onChange={callback} type="card"> <TabPane TAB =" all "keys ="1"> <TodoList TodoList ={todoList} /> </TabPane> <TabPane TAB =" in progress "key="2"> < todoList todoList={activeTodoList} /> </TabPane> <TabPane TAB =" completed "key="3"> <TodoList TodoList ={completedTodoList} /> </TabPane> </Tabs> </div> //...Copy the code

As you can see, the above content has been modified as follows:

  • First we delete itTodoListPart of the code was imported insteadTodoListcomponent
  • And then we useuseStateHooks to receivetodoListDataAs the default data, and then passisCompletedFilter, generate

summary

Let’s summarize and review what we learned in this section:

  • First of all, we looked at functions in TS, and we looked at how to annotate functions
  • Then it introduces how to annotate the function type of a variable when a function is assigned to a variable, and thus explains the ability of TS to automatically infer the type
  • Next, we showed that functions also have optional parameters for standard interfaces
  • Finally, we explained the overloading of TS in particular, it is mainly used to solve the function arguments exist a variety of types, and then the corresponding parameters of different types have different return value type, then we will give this function for type annotations, can through the way of overloading, decoupling parameter value type and returns a value type, can present all possible situations by overloading.

Because this article is a cup of tea series in The Tuq community, we still have some content about the function knowledge, but the specific content is by analogy, such as annotation function rest parameters, this, etc., interested students can refer to the official document: TS- function.

Cross type, union type

In the first three big chapters, we looked at the basic TS types, and then we used those basic types to combine enumerations and interfaces, and to annotate function arguments and return values. This is the practice of annotating TS types on JS elements. So just as there are element operations in JS such as addition, subtraction, multiplication, division, and even set operations “interchange and complement,” there is also an operation of type in TS, and that’s the crossover and union types we’ll be looking at in this section.

Cross type

Cross type is more than one type, through type & operator, merged into one type of this type contains multiple types of all types of members, let’s look at an example of response body, if we have a query is the artist’s request, we will according to the results of the query – response body, print the corresponding information, general response body is a combination of two types of information:

  • Request success returns the status indicating success of the request, along with target data
  • Failed request returns a status indicating that the request failed, along with an error message

For this one scenario, we can use cross types. Now that we know about such a scenario, let’s look at a specific example for this scenario:

interface ErrorHandling {
  success: boolean; error? : { message:string };
}

interface ArtistsData {
  artists: { name: string} []; }const handleArtistsResponse = (response: ArtistsData & ErrorHandling) = > {
  if (response.error) {
    console.error(response.error.message);
    return;
  }

  console.log(response.artists);
};
Copy the code

We can see in this example that our artist information Interface is ArtistsData, which is one of the concrete data that is returned after a successful request. In addition to this, our response body usually has a status that indicates whether the response was successful or not, and a printed message that indicates if the response was wrong. So we also define ErrorHandling, and the two make up our artist response body by performing an intertype operation: ArtistsData & ErrorHandling, then we mark response as the result of this cross type in the function argument and print the corresponding information in the function body based on the reponse.error judgment of whether the request was successful or not.

The joint type

So what is the union type? Joint type is actually through the operator | to multiple types of joint, forming a complex type, when using the composite type annotations when a variable, the variable can take any one type of the compound type, this is similar to enumeration, is a variable may have multiple types, but in the end can only take one type.

One of the details of the difference between union types and enumerated types is for the reader to see for himself.

For example, we have a padLeft function that takes two parameters, value and padding. The main goal is to add the padding to the left of the value string. The padding can be a space, but the padding can be either a string or a number. When the padding is a string, a simple example is as follows:

const value: string = 'Hello Tuture';
const padding: string = ' ';

padLeft(value, padding) // => ' Hello Tuture';
Copy the code

Ok, now that we know the scenario, let’s go ahead and explain an updated version of the above example:

function padLeft(value: string, padding: any) {
  if (typeof padding === "number") {
    return Array(padding + 1).join("") + value;
  }
  if (typeof padding === "string") {
    return padding + value;
  }
  throw new Error(`Expected string or number, got '${padding}'. `);
}

padLeft("Hello world".4);
Copy the code

You can see in this example, the padding is given by any for the moment, and then the body of the function determines the string and number types and performs the corresponding “left space fill” operation. This logic works for early development, but when it comes to collaborative development, Other members can’t figure out what type of value to pass to the padding function just by looking at the function’s variable definition.

padLeft('Hello world'.true)
Copy the code

Bam! The program crashes! So you see, the program is still fragile.

To more explicitly constrain the padding type, it is necessary to introduce a union type:

function padLeft(value: string, padding: string | number) {
  / /... In the middle as
}
Copy the code

At this point, we find that even if there are many more teammates, they will know how to call the interface, because the compiler will force teammates to write the correct type if they continue:

padLeft('Hello world'.true)
Copy the code

The compiler will prompt you with the following error:

summary

In this section, we studied the cross type and joint type, they are the product of the type of operation of the TS type system, cross type is more than one type of a type, type of the end result is the sum of multiple types, and the joint type is a combination of multiple types, the final result type is more than one type of a certain type, The crossover type is used to construct response bodies, the union type is used to handle scenarios where a single variable is annotated as one of multiple types, and it also interacts chemically with literal types, implementing enumerations, and handling type guards, which we’ll cover in a moment.

Literal types and type guards

Finally, let’s talk about type guards, which are used in conjunction with union types in many scenarios. We also need to talk about literal types before we talk about type guards. Ugh! In fact, the three are complementary.

Literal type

In fact, literal types have been more or less mentioned in section 2. Remember that error?

const tutureSlogan: string = 5201314 Type '5201314' is not assignable to Type 'string'
Copy the code

The TS compiler suggests that “Type ‘5201314’ is not assignable to Type ‘string ‘.

Literals are the smallest type in the TS system. Like the number 1 in JS, they cannot be broken into smaller parts. There are two types of literals:

  • Numeric literal
  • String literals

Numeric literal

A number like 520, used as a type, is an array literal type, and is used to annotate a variable like this:

let tuture: 520
Copy the code

When we initialize the tuture variable, we can only assign the number 520:

tuture = 520; / / right
tuture = 521; // Error Type '521' is not assignable to Type '520'
Copy the code

String literals

The corresponding string literal is similar, we now annotate tuture with the string literal type ‘520’ :

let tuture: '520';

tuture = '520';
tuture = '521'; // Type '"521"' is not assignable to type '"520"'
Copy the code

As you can see, a further feature of the literal type is that the annotated variables that correspond to the literal type can only be assigned to the annotated literal.

Now that we’ve looked at literal types and talked about their characteristics, what’s so special about such a simple type? Here are two examples of the unexpected power of combining literal types with union types:

  • To realize the enumeration
  • Implement type guarding

Collocation examples – implement enumeration effect

When we match the joint type and literal type, we can achieve a certain effect of enumeration, let’s look at an example, we are generally three types of buying a computer system, we can choose the three types of computers to obtain corresponding to a user’s situation, we now give only a general frame, function concrete realization in type guard in detail:

function getUserInfo(osType: 'Linux' | 'Mac' | 'Windows') { // ... Subsequent implementation}Copy the code

As we can see in the example above, osType can take a value from one of three operating systems. This is similar to enumeration, and we can create a similar enumeration:

enum EnumOSType {
  Linux,
  Mac,
  Windows
}

function getUserInfo(osType: EnumOSType) {}
Copy the code

The above two examples have similar effects. We have achieved a simple enumeration effect by combining the type with the literal type.

Type the guards

Type of guard is our joint type + literal type of another application scenario, it is mainly used for the “joint” between multiple types, having the same field, also exist in different fields, and then need to distinguish between specific when it is used which type, so may be confused, let’s look at an example, The parameter added to our getUserInfo function receives the OS and prints the user information carried by the OS according to os.type:

interface Linux {
  type: 'Linux';
  linuxUserInfo: 'geek';
}

interface Mac {
  type: 'Mac';
  macUserInfo: 'geek + 1';
}

interface Windows {
  type: 'Windows';
  windowsUserInfo: 'geek + 2';
}

function getUserInfo(os: Linux | Mac | Windows) {
  console.log(os.linuxUserInfo);
}
Copy the code

OS type = xxxUserInfo; OS type = xxxUserInfo; OS type = xxxUserInfo;

Some students have a question, we are not a joint type, then OS should have Linux this type ah, so why would be wrong to print? The point to make is that the end result of a federated type is one of multiple federated types, that is, the OS could be Mac or Windows, so there is a problem with printing OS.linuxUserinfo, so we need a type guard at this point, It is based on the same field in multiple types, and the field is a literal type, and then performs different logic to ensure that the type is executed correctly. Let’s extend the above example:

function getUserInfo(os: Linux | Mac | Windows) {
  switch (os.type) {
    case 'Linux': {
      console.log(os.linuxUserInfo);
      break;
    }

    case 'Mac': {
      console.log(os.macUserInfo);
      break;
    }

    case 'Windows': {
      console.log(os.windowsUserInfo);
      break; }}}Copy the code

Type = OS. Type = OS. Type = OS. Type = OS. Type = OS. Linux will be prompted:

Hands-on practice

Once we know about literal types and type guards, we immediately apply them to our to-do list application.

SRC/todolist.tsx: todolist.tsx: todolist.tsx: todolist.tsx

import React from "react"; import { List, Avatar, Menu, Dropdown, Modal } from "antd"; import { DownOutlined, ExclamationCircleOutlined } from "@ant-design/icons"; import { ClickParam } from "antd/lib/menu"; import { Todo, getUserById } from "./utils/data"; const { confirm } = Modal; interface ActionProps { onClick: (key: "complete" | "delete") => void; isCompleted: boolean; } function Action({ onClick, isCompleted }: ActionProps) { const handleActionClick = ({ key }: ClickParam) => { if (key === "complete") { onClick("complete"); } else if (key === "delete") { onClick("delete"); }}; return ( <Menu onClick={handleActionClick}> <Menu.Item key="complete">{isCompleted ? "Redo" : "complete"} < / Menu Item > <. Menu Item key = "delete" > delete < / Menu Item > < Menu >); } interface TodoListProps { todoList: Todo[]; onClick: (todoId: string, key: "complete" | "delete") => void; } function TodoList({ todoList, onClick }: TodoListProps) { return ( <List className="demo-loadmore-list" // ... <List.Item key={item.id} actions={[ <Dropdown overlay={() => ( <Action isCompleted={item.isCompleted} onClick={(key: "Complete" | "delete") = > onClick (item id, key)} / >)} > < a key = "list - loadmore - more" > action < DownOutlined / > < / a > / /...Copy the code

As you can see, the above changes mainly include the following parts:

  • We extended the menu component of a single Todo click drop-down menu, defining oneActionComponent, which takes two arguments,isCompletedonClick, the former is used to indicate whether the Todo operation is being redone or completed, and the latter is used to handle the click event, according totodo.idAnd the type of operationkeyTo deal with.
  • We are inActionThe component’sonClickPropertyonClickFunctions are passed down from the parent component, so we need additional functions inTodoListPropsCombined with theonClickThe type definition of a function, and in accordance with what we learned about annotation functions, here we need the annotation argument list and the return value becauseonClickThe function performs click logic internally and does not return a value, so we annotate itvoidType, for parameter lists,todoIdIt’s pretty simple, it’s usually a string, so the annotation isstringType, andkeyThe type of the annotation operation, which is a literal union type, is allowedcompletedeleteTwo kinds of
  • Next, let’s look at the Action component. We explained in the previous step that it takes two parameters, so let’s add oneActionPropsTo annotate the parameter list of the Action component, see theonClickAs we explained in the last step,isCompletedAnnotated asboolean
  • Then in the Action component we define MenuonClickHandler function ofhandleActionClickIs aClickParamType, which is fromantd/lib/menuImported, provided by the component library, and then we deconstruct it from the parameterskey, in turn, type guards through literal types, handling the values ofonClicklogic
  • The last improvement we made was to use the MenuisCompletedShow “redo” or “finish”.

TSX: SRC /App. TSX: TodoList /App. TSX: TodoList /App. TSX: TodoList /App.

import React, { useRef, useState } from "react"; import { List, Avatar, // ... function App() { const [todoList, setTodoList] = useState(todoListData); const callback = () => {}; / /... const activeTodoList = todoList.filter(todo => ! todo.isCompleted); const completedTodoList = todoList.filter(todo => todo.isCompleted); const onClick = (todoId: string, key: "complete" | "delete") => { if (key === "complete") { const newTodoList = todoList.map(todo => { if (todo.id === todoId)  { return { ... todo, isCompleted: ! todo.isCompleted }; } return todo; }); setTodoList(newTodoList); } else if (key === "delete") { const newTodoList = todoList.filter(todo => todo.id ! == todoId); setTodoList(newTodoList); }}; return ( <div className="App" ref={ref}> <div className="container header"> // ... <div className="container"> <Tabs onChange={callback} type="card"> <TabPane TAB =" all "keys ="1"> <TodoList TodoList ={todoList} onClick={onClick} /> </TabPane> <TabPane TAB =" in progress "key="2"> < todoList todoList={activeTodoList} OnClick ={onClick} /> </TabPane> <TabPane TAB =" completed "key="3"> <TodoList TodoList ={completedTodoList} onClick={onClick} /> </TabPane> </Tabs> </div> </div> ); } export default App;Copy the code

As you can see, there are two main changes:

  • toTodoListincreaseonClickattribute
  • implementationonClickFunction, according to literal typekeyPerform type guarding to handle the corresponding data change logic

summary

In this summary, we studied the literal type and type of guard, literal type and combined type collocation can achieve the effect of enumeration, can also handle type guard, literal type is the most types of atoms in the TS, it can not be disassembled, and type of guard is mainly on the joint type, TS compiler can’t handle, The TS compiler needs to be manually assisted by the developer to handle the type.

Want to learn more exciting practical skills tutorial? Come and visit the Tooquine community.

The source code for this article is available on Github or Gitee. If you think it is well written, please give Github or Gitee a thumbs up at ❤️ or ❤️