Engineering practice of typescript on vue3

Time:2020-11-28

Preface

How to… Cough, we don’t want to be crooked, this is a pure technical article.

0.1 Why TypeScript

what? Do you want to change Vue 3.0 to typescript? Isn’t this teasing me? Am I going to use typescript to write Vue applications?

Engineering practice of typescript on vue3

Well, vue3.0 may not come out until the end of 19 years at the earliest. Vue3.0 will be more user-friendly to TS users, not just ts. the reason why we use TS more often is that TS static type detection and TS performance is better than flow. Since the giant hard stride towards open source, there are many new tools in the front-end circle, such as vs Code and typescript. I think typescript is really popular because the complexity of front-end applications is soaring, which brings about the problem of poor maintainability and scalability. Especially when writing the class library, we need to consider the reusability and extensibility of various classes and methods, so we will use design patterns to optimize the code. What’s more, with the improvement of coding efficiency, the static system undoubtedly reduces the debugging time.

0.2 Advantages & Disadvantages

advantage

  • Static type system, with the help of compiler, can handle errors during compilation, avoid errors that may occur at runtime in advance, and improve the reliability of the code.
  • Secondly, if the data type is determined in the program, the compiler can optimize the program according to the information. (typescript is compiled into JavaScript and optimized for the basic data type of JS).
  • There are many tools in the community, VS code support is very powerful, type hints and Reference markings are awesome, developers tools and experience can be said to do well in the JS world.

shortcoming

  • Learning curve, for programmers who do not have static language background such as Java / C + +, there may be an adaptation period.
  • As a static type language, typescript requires programmers to write programs according to the contract and specify the type for each variable. In addition to the basic types of JavaScript such as string and number, it also needs to declare the type for the composite structure through the interface keyword.
  • Type declaration adds more code, and these details distract programmers from business logic during programming.
let foo = 123;
foo = '456'; // Error: cannot assign `string` to `number
  • Typescript supports the new features of es2015 +. With the development of standards, new features will be added to typescript continuously. Using typescript can avoid the risk of using new features in some browsers with low version by compiling.

1. Engineering practice


1.1 cliche on webpack configuration

Webpack has been released to version 4.41. I believe many partners have already installed webpack 4. Webpack 4 supports typescript 8 wrong. Its biggest changes are “zero configuration” and embedding the commonchunks plugin plug-in as webpack built-in. Latest version:
Engineering practice of typescript on vue3

  1. The first step is to install typescript. Typescript is a superset of JavaScript, which has many features or syntax sugar that are not native to it. At the same time, the browser cannot run it directly. It needs to have a compilation process, that is, to compile typescript into JavaScript, so typescript needs to be installed first
npm install -g typescript
  1. Then try to compile. After local installation, you can compile the file with suffix. Ts, and output it as standard JavaScript file.

Suppose we have a student class written in typescript.

class Student {
  private name: string;
  constructor(name: string) {            
    this.name = name;
  }
}

Use typescript compiler to compile it

tsc student.ts

The compiled result is a standard JavaScript file generated according to the compilation options.

var Student = /** @class */ (function () {
    function Student(name) {
        this.name = name;
    }
    return Student;
}());
  1. Command line compilation is suitable for a single or small number of typescript files. If you want to use typescript to write a large application or class library, you need to configure webpack to automatically compile the whole project when building. To configure a typescript project with webpack, the following process is followed:
Typescript > JavaScript version of ES next > JavaScript with good compatibility.

Engineering practice of typescript on vue3

It’s worth noting

The typescript compiler has been installed before. It is usually specified in the compiler option that typescript is to be compiled to a JavaScript version that supports Es5 / ES6 / es next. However, in practice, we still need to use the result of Babel for another translation. There are two reasons for this.

  1. The results compiled by typescript compiler can not be directly used in production environment. Using Babel, JS code with compatibility suitable for production environment can be translated through browserlist.
  2. Babel can introduce Polyfill. Usually, the compilation target of typescript is set to es next. Then Babel can introduce Polyfill as needed to minimize the volume of JS code generated.

const path = require('path')
const webpack = require('webpack')
const config = {
  entry: './src/index.ts',
  module: {
    rules: [
      {
        // ts-loader: convert typescript to javascript(esnext),
        // babel-loader: converts javascript(esnext) to javascript(backward compatibility)
        test: /\.(tsx|ts)?$/,
        use: ['babel-loader', 'ts-loader'],
        exclude: /node_modules/
      },
    ]
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
    alias: {
      '@': path.resolve(__dirname, './src'),
      'mobx': path.resolve(__dirname, './node_modules/mobx/lib/mobx.es6.js')
    }
  },
}

1.2 typescript compiler configuration

This paper briefly introduces the compilation options of typescript. It usually specifies the compilation target JS version, the modularization mode of code and the code checking rules.

  • allowJSIndicates whether JavaScript files are allowed to be compiled.
  • targetRepresents the target version of ECMAScript, such as’ esnext ‘,’ es2015 ‘.
  • moduleMeans modularity, such as’ commonjs’, ‘UMD’, or ‘es2105’ (ES module)
  • moduleResolutionIt indicates the module resolution strategy, that is to tell the compiler where to find the current module. When ‘node’ is specified, nodejs module resolution strategy is adopted. The complete algorithm can be implemented in the Node.js Module documentation is found. When its value is specified as’ classic ‘, the default parsing strategy of typescript is adopted. This strategy is mainly for compatibility with older versions of typescript.
  • strictWhether to start all strict type checking selection, including ‘noimplicitany’, ‘noimplicitthis’.
  • libRepresents the list of library files that need to be imported during the compilation process, which is imported according to the actual application scenario.
  • experimentalDecoratorsIt is an option to support decorator syntax. Since mobx is used for state management in the project, it is necessary to enable decorator syntax.
  • includeOption represents the compiled directory
  • outDirRepresents the directory for the output of the compilation results.

{
    "compileOnSave": true,
    "compilerOptions": {
        "target": "esnext",
        "module": "esnext",
        "moduleResolution": "node",
        "sourceMap": true,
        "strict": true,
        "allowJs": true,
        "experimentalDecorators": true,
        "outDir": "./dist/",
        "lib": [
          "es2015", "dom", "es2016", "es2017", "dom.iterable", "scripthost", "webworker"
        ]
    },

    "include": [
        "src/**/*.ts"
    ]
}

1.3 tslint practice

Tslint is a lint tool for typescript. Similar to eslint, it follows airbnb style or standard style. Eslint can also specify the typescript specification to follow. At present, there are three built-in presets in tslint,recommendedlatestas well asallIt saves us the trouble of configuring each rule of tslint.

  • recommendedIt is a stable version of the rule set. It is better to use it in general typescript projects, and it follows semver.
  • latestIt is updated to include the configuration of the latest rules in each tslint version, and this configuration will be updated with the release of tslint break change.
  • allConfigure all rules to the most stringent configuration.

Tslint rule

Tslint rules are divided into severity levels, and each rule can be configureddefault error warningoroff。 Tslint presupposition provides a lot of rules refined from code practice. I think there are several rules that we will often encounter or need to pay attention to.

  • only-arrow-functionsOnly arrow functions are allowed, and traditional function expressions are not allowed.
  • promise-function-asyncAny function or method that returns promise should be identified with ‘async’;
  • await-promiseIf the value followed by the ‘wait’ keyword is not promise, it will warn us and standardize the writing of our asynchronous code.
  • no-consoleIt is forbidden to use the ‘console’ method in the code to remove useless debugging code.
  • no-debuggerUse of the ‘debugger’ method in code is prohibited, as above.
  • no-shadowed-variableWhen there is a variable with the same name in the local scope and the outer scope, it is called shadowing, which will make the local scope unable to access the variable with the same name in the outer scope.
  • no-unused-variableUnused variables, imports, functions, etc. are not allowed. The significance of this rule is to avoid compilation errors, and it also leads to confusion among readers because variables are declared but not applicable.
  • max-line-lengthThe number of words per line is required to be limited;
  • quotemarkSpecifies the symbol to be used for string constants. Generally, ‘single’ is specified. This depends on the team style.
  • prefer-constIf possible, use ‘const’ to declare variables instead of ‘let’. For variables that will not be assigned repeatedly, ‘const’ is used by default;

For other rules, you can refer to the official tslint document in detail. Using lint can better standardize the code style, maintain the unity of team code style, avoid the problem of easily causing compilation errors, and improve the readability and maintainability.


Special flags of tslint

When we write code with TS, we often encounter the situation that the number of words in a line of code is too long. In this case, we can use the flag provided by tslint to make the line not subject to the rules.


// tslint:disable-next-line:max-line-length

  private paintPopupWithFade<T extends THREE.Object3D>(paintObj: T, popupStyleoption: PopupStyleOption, userDataType: number) {

  //...

}

In fact, tslint indicates that the number of words in the line violates the max line length rule. Here, you can add comments// tslint: disable-next-line: rulexTo disable this rule.

2. Typescript type system pitfall tips


2.1 “duck” type

“Duck” type?? (black question mark), I was confused when I first saw this term. In fact, it refers to structural type. At present, type detection is mainly divided into structural type and nominal type.

interface Point2D {
  x: number;
  y: number;
}
interface Point3D {
  x: number;
  y: number;
  z: number;
}
var point2D: Point2D = { x:0, y: 10}
var point3D: Point3D = { x: 0, y: 10, z: 20}

function iTakePoint2D(point: Point2D) { /*do sth*/ }

Itakepoint2d (point2d); // type matching
Itakepoint2d (point3d); // type compatible, structure type
Itakepoint2d ({X: 0}); // error: missing information ` y`

difference

  • The type detection and judgment of structural type is based on the structure of the type. It will see what properties it has and what type it is, rather than the name of the type or the ID of the type.
  • Nominal types are used by static languages such as Java and C. in short, if the type names of two types are different, then the two types are different types, although the two types have the same structure.
  • Types in typescript are structural types. Type checking focuses on the shape of value, that is, duck type. In general, defining types through interface is actually to define shapes and constraints. Therefore, defining interface is actually to define new types for structures. For typescript, two types are the same type as long as they have the same structure.

2.2 type judgment / classification

Once we know that typescript is a duck type, we will think of a problem: tsDuck typeHow to judge the type, such as the following example:

  public convertString2Image(customizeData: UserDataType) {
    if (Helper.isUserData(customizeData)) {
      const errorIcon = searchImageByName(this.iconImage, statusIconKey);
      if (errorIcon) {
        (customizeData as UserData).title.icon = errorIcon;
      }
    } else if (Helper.isUserFloorData(customizeData)) {
      // do nothing
    } else {
      // UserAlertData
      let targetImg;
      const titleIcon = (customizeData as UserAlertData)!.title.icon;
      if (targetImg) {
        (customizeData as UserAlertData).title.icon = targetImg;
      }
    }
    return customizeData;
  }

The method is to fill in the icon field with the actual corresponding image according to the incoming user data,customizeDataIt is user data. At this time, we need to call it according to different typessearchImageByNameMethod to load the corresponding image, so we need to determine the type of the object at runtime through some type judgment methods.

Type judgment of foundation

We may think of the basic type judgment methodtypeofandinstanceofIn TS, you can also use these two operators to determine the type, for example:

  • usetypeofJudgment type

function doSomething(x: number | string) {
  if(typeof x === 'string') {
      console.log(x.toFixed()); // Property 'toFixed' does not exist on type 'string'
      console.log(x.substr(1));
  } else if (typeof x === 'number') {
      console.log(x.toFixed());
      console.log(x.substr(1)); // Property 'substr' does not exist on type 'number'.
  }
}

You can see the use oftypeofIt is feasible to judge the basic data type at runtime, and different business logic can be executed for different types in different condition blocksClassperhapsInterfaceOther methods must be considered.

  • useinstanceofJudgment type

The following example is based on thegeoObject types perform different processing logic:

  public addTo(geo: IMap | IArea | Marker) {
    this.gisObj = geo;
    this.container = this.draw()!;
    if (!this.container) {
      return;
    }
    this.mapContainer.appendChild<HTMLDivElement>(this.container!);
    if (this.gisObj instanceof IMap) {
      this.handleDuration();
    } else if(this.gisObj instanceof Marker) {
      //
    }
  }

As you can see, useinstanceofIt is feasible to judge the type dynamically, and the type can beClassKeyword declared types that have complex structures and have constructors. In general, usinginstanceofThe two conditions for judging the type are as follows:

  1. It must be a type that has a constructor, such as a class type.
  2. ConstructorprototypeProperty type cannot beany

Using type predicates to judge types
With the example at the beginning, we are going to judge a duck type. In TS, we have a special way, that isType predicateThe concept of (type predict), which is the type protection mechanism of typescript, will check the type in a specific scope at run time. For thoseInterfaceDefined type and mapped type, and it does not have a constructor, so we need to define the checking method of this type, which is also calledType protection

The implementation of two type protection based methods for the call in the example

  public static isUserData(userData: UserDataType): userData is UserData {
    return ((userData as UserData).title !== undefined) && ((userData as UserData).subTitle !== undefined)
      && ((userData as UserData).body !== undefined) && ((userData as UserData).type === USER_DATA_TYPE.USER_DATA);
  }
  public static isUserFloorData(userFloorData: UserDataType): userFloorData is UserFloorData {
    return ((userFloorData as UserFloorData).deviceAllNum !== undefined)
      && ((userFloorData as UserFloorData).deviceNormalNum !== undefined)
      && ((userFloorData as UserFloorData).deviceFaultNum !== undefined)
      && ((userFloorData as UserFloorData).deviceOfflineNum !== undefined);
  }

In fact, we have to judge the structure of this type, which is why the type system of TS is calledDuck typeWe need to traverse each property of the object to distinguish the type. In other words, if two types with exactly the same structure are defined, the same type will be judged even if the type name is different~

2.3 what is the index type for?

Index types. With the index type, the compiler can check the code that uses the dynamic property name. Accessing operators by index in TSkeyofGet the property name in the type, such as the following example:

function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
    return names.map(n => o[n]);
}
​
interface Person {
  name: string;
  age: number;
}
let person: Person {
  name: 'Jarid',
  age: 35
}
let strings: string[] = pluck(person, ['name']);

principle
The compiler checksnameIs it truepersonAnd thenkeyof T, index type query operator, for any type T,keyof TThe result of is a union of known attribute names on t.

let personProps: keyof Person; // 'name' | 'age'

In other words, the property name can also be any interface type!

Index access operator T [k]

Index type refers to that the property in TS can be dynamic type, and the type is known only when it is evaluated at run time. You can use it in a normal contextT[K]Type, just make sureK extends keyof TThen, for example:

function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
    return o[name];
}

Principle:o:Tandname:Kexpresso[name]: T[K]When you returnT[K]GetKey will change the type of the result as the compiler instantiates it.

let name: string = getProperty(person, 'name');
let age: number = getProperty(person, 'age');
let unknown = getProperty(person, 'unknown'); // error, 'unknown' is not in 'name' | 'age'

Index type and string index signature
keyofandT[k]Interact with string index signatures.  
For example:

interface Map<T> {
    [key: String]: T; // this is a type with string index signature, keyof t is string
}
let keys: keyof Map<number>; // string
let value: Map<number>['foo']; // number

Map<T>Is a type with a string index signature, then keyof T will be string.

2.4 mapping type

background
When using typescript, there is a problem that we can’t get around — how to create a new type from an old type, that is, a mapped type.

interface PersonPartial {
    name?: string;
    age?: number;
}

interface PersonReadonly {
    readonly name: string;
    readonly age: number;
}

You can seePersonReadOnlyThis type is just rightPersonParialType’s fields are read-only. Imagine if the type has 10 fields, you need to write these 10 fields repeatedly. Is there any way that we can get new types by mapping instead of repeating this template code? The answer is the mapping type,

Principle of mapping type
The new type converts each property of the old type in the same way:

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

Its syntax is similar to that of index signature. It has three steps

  1. Type variable K, bound to each property in turn.
  2. String literal associativeKeysContains the collection of property names to iterate
  3. The type of the property.

Take the following example

type Keys = 'option1' | 'option2';
type Flags = { [K in keys]: boolean };

KeysIs a hard coded string of attribute names, and then the type of this attribute is Boolean, so this mapping type is equivalent to:

type Flags = {
    option1: boolean;
    option2: boolean;
}

characteristic use
What we often encounter, or more commonly, is (generic writing)

type Nullable<T> = { [P in keyof T]: T[P] | null }

Declare a person type. Once converted with nullable type, each property of the new type obtained is a type that allows nulls.


// test
interface Person {
    name: string;
    age: number;
    greatOrNot: boolean;
}
type NullPerson = Nullable<Person>;

const nullPerson: NullPerson = {
    name: '123',
    age: null,
    greatOrNot: true,
};

Operation
By using type mapping, we can implement thePickandOmitPickIt is the type of TS, such as the following example:

export interface Product {
  id: string;
  name: string;
  price: string;
  description: string;
  author: string;
  authorLink: string;
}

export type ProductPhotoProps = Pick<Product, 'id' | 'author'| 'authorlink' | 'price'>;

//Implementation of omit
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

export type ProductPhotoOtherProps = Omit<Product, 'name' | 'description'>;

We can take what we already haveProductSeveral types of typespickWe can also ignore several types and form a new type with the remaining attributes.

benefit

  • Keyof t returns the attribute list of T, and T [P] is the result type. This type conversion will not be applied to other attributes in the prototype chain, which means that the mapping will only be applied to the attributes of T and not to other attributes in the prototype chain. The compiler copies all existing property modifiers before adding new properties.
  • Both properties and methods can be mapped.

2.5 never type vs void type

never
first,neverThere are two types of scenarios:

  • When a function returns a value, it means that there will never be a return value.
  • Represents a function that always throws errors.
//A function that returns never must have an unreachable endpoint
function error(message: string): never {
    throw new Error(message);
}
//The inferred return value type is never
function fail() {
    return error("Something failed");
}

void
voidThere are also application scenarios

  • When a function has no return value, usually typescript will automatically consider its return valuevoid
  • Declare in codevoidType or return value marked withvoidIt can improve the readability of the code, make it clear that the method will not have a return value, and can avoid paying attention to the return value when writing tests.
  public remove(): void {
    if (this.container) {
      this.mapContainer.removeChild(this.container);
    }
    this.container = null;
  }

Summary

  • neverEssence represents types that never have a value, and can also represent the return value of a function expression or an arrow function expression.
  • We can define a function or variable asvoidType, variable can still be assignedundefinedornull, butneverYes can only be returned with a value ofneverFunction assignment.

2.6 enumeration type

In TSenumIt seems that enumerations exist in many strongly typed languages. However, JavaScript does not. Enumerations can help us to replace those codes with meaningful namesmagic numberOr a value with a specific meaning. Here is an enumeration type used in our business:

export enum GEO_LEVEL {
  NATION = 1,
  PROVINCE = 2,
  CITY = 3,
  DISTRICT = 4,
  BUILDING = 6,
  FLOOR = 7,
  ROOM = 8,
  POINT = 9,
}

Because the values are allnumberAlso known as numeric enumeration.

Numerical based enumeration
The enumerations of TS are all based on the numerical type, and the values can be assigned to the enumeration, for example:

enum Color {
    Red,
    Green,
    Blue
}
var col = Color.Red;
Col = 0; // and Color.Red The effect is the same

TS internal implementation
Let’s take a look at how the enumeration type whose enumeration value is a numeric type can be converted to javascript:

//Translated JavaScript
define(["require", "exports"], function (require, exports) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    var GEO_LEVEL;
    (function (GEO_LEVEL) {
        GEO_LEVEL[GEO_LEVEL["NATION"] = 1] = "NATION";
        GEO_LEVEL[GEO_LEVEL["PROVINCE"] = 2] = "PROVINCE";
        GEO_LEVEL[GEO_LEVEL["CITY"] = 3] = "CITY";
        GEO_LEVEL[GEO_LEVEL["DISTRICT"] = 4] = "DISTRICT";
        GEO_LEVEL[GEO_LEVEL["BUILDING"] = 6] = "BUILDING";
        GEO_LEVEL[GEO_LEVEL["FLOOR"] = 7] = "FLOOR";
        GEO_LEVEL[GEO_LEVEL["ROOM"] = 8] = "ROOM";
        GEO_LEVEL[GEO_LEVEL["POINT"] = 9] = "POINT";
    })(GEO_LEVEL = exports.GEO_LEVEL || (exports.GEO_LEVEL = {}));
});

It’s very interesting. Let’s not think about why we need to translate like this, but think from another angle. In fact, the code above illustrates such a thing:

console.log(GEO_LEVEL[1]); // 'NATION'
console.log(GEO_LEVEL['NATION']) // 1
// GEO_LEVEL[GEO_LEVEL.NATION] === GEO_LEVEL[1]

So we can actually use this enumeration variable Geo_ Level to convert the enumeration represented by subscript tokeyRepresents an enumeration of,keyThe enumeration represented by can also be converted to a subscript.

3. Reference

design pattern in typescript

typescript deep dive

tslint rules

Typescript Chinese document

Typescript advanced type

you might not need typescript

advanced typescript classes and types