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?
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:
- 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
- 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;
}());
- 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.
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.
- 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.
- 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.
-
allowJS
Indicates whether JavaScript files are allowed to be compiled. -
target
Represents the target version of ECMAScript, such as’ esnext ‘,’ es2015 ‘. -
module
Means modularity, such as’ commonjs’, ‘UMD’, or ‘es2105’ (ES module) -
moduleResolution
It 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. -
strict
Whether to start all strict type checking selection, including ‘noimplicitany’, ‘noimplicitthis’. -
lib
Represents the list of library files that need to be imported during the compilation process, which is imported according to the actual application scenario. -
experimentalDecorators
It is an option to support decorator syntax. Since mobx is used for state management in the project, it is necessary to enable decorator syntax. -
include
Option represents the compiled directory -
outDir
Represents 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,recommended
、latest
as well asall
It saves us the trouble of configuring each rule of tslint.
-
recommended
It is a stable version of the rule set. It is better to use it in general typescript projects, and it follows semver. -
latest
It 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. -
all
Configure all rules to the most stringent configuration.
Tslint rule
Tslint rules are divided into severity levels, and each rule can be configureddefault
error
warning
oroff
。 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-functions
Only arrow functions are allowed, and traditional function expressions are not allowed. -
promise-function-async
Any function or method that returns promise should be identified with ‘async’; -
await-promise
If the value followed by the ‘wait’ keyword is not promise, it will warn us and standardize the writing of our asynchronous code. -
no-console
It is forbidden to use the ‘console’ method in the code to remove useless debugging code. -
no-debugger
Use of the ‘debugger’ method in code is prohibited, as above. -
no-shadowed-variable
When 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-variable
Unused 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-length
The number of words per line is required to be limited; -
quotemark
Specifies the symbol to be used for string constants. Generally, ‘single’ is specified. This depends on the team style. -
prefer-const
If 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: rulex
To 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 type
How 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,customizeData
It is user data. At this time, we need to call it according to different typessearchImageByName
Method 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 methodtypeof
andinstanceof
In TS, you can also use these two operators to determine the type, for example:
- use
typeof
Judgment 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 oftypeof
It is feasible to judge the basic data type at runtime, and different business logic can be executed for different types in different condition blocksClass
perhapsInterface
Other methods must be considered.
- use
instanceof
Judgment type
The following example is based on thegeo
Object 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, useinstanceof
It is feasible to judge the type dynamically, and the type can beClass
Keyword declared types that have complex structures and have constructors. In general, usinginstanceof
The two conditions for judging the type are as follows:
- It must be a type that has a constructor, such as a class type.
- Constructor
prototype
Property 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 predicate
The concept of (type predict), which is the type protection mechanism of typescript, will check the type in a specific scope at run time. For thoseInterface
Defined 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 type
We 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 TSkeyof
Get 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 checksname
Is it trueperson
And thenkeyof T
, index type query operator, for any type T,keyof T
The 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 T
Then, for example:
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
return o[name];
}
Principle:o:T
andname:K
expresso[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 signaturekeyof
andT[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 seePersonReadOnly
This type is just rightPersonParial
Type’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
- Type variable K, bound to each property in turn.
- String literal associative
Keys
Contains the collection of property names to iterate - The type of the property.
Take the following example
type Keys = 'option1' | 'option2';
type Flags = { [K in keys]: boolean };
Keys
Is 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 thePick
andOmit
,Pick
It 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 haveProduct
Several types of typespick
We 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,never
There 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");
}
voidvoid
There are also application scenarios
- When a function has no return value, usually typescript will automatically consider its return value
void
。 - Declare in code
void
Type or return value marked withvoid
It 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
-
never
Essence 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 as
void
Type, variable can still be assignedundefined
ornull
, butnever
Yes can only be returned with a value ofnever
Function assignment.
2.6 enumeration type
In TSenum
It seems that enumerations exist in many strongly typed languages. However, JavaScript does not. Enumerations can help us to replace those codes with meaningful namesmagic number
Or 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 allnumber
Also 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 tokey
Represents an enumeration of,key
The 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