1. What is TypeScript
TypeScript is a free and open source programming language developed by Microsoft. It is a superset of JavaScript, and essentially adds optional static typing and class-based object-oriented programming to the language.
TypeScript provides the latest and evolving JavaScript features, including those from ECMAScript in 2015 and future proposals, such as asynchronous functions and Decorators, to help build robust components. The following figure shows the relationship between TypeScript and ES5, ES2015, and ES2016:
1.1 The difference between TypeScript and JavaScript
TypeScript | JavaScript |
---|---|
A superset of JavaScript is used to solve the code complexity of large projects | A scripting language used to create dynamic web pages |
Errors can be found and corrected during compilation | As an interpreted language, errors can only be found at runtime |
Strong typing, supports static and dynamic typing | Weak type, no static type option |
It is finally compiled into JavaScript code, so that the browser can understand | Can be used directly in the browser |
Support modules, generics and interfaces | Does not support modules, generics or interfaces |
The support of the community is still growing, and it's not very big | A lot of community support and a lot of documentation and problem-solving support |
1.2 Get TypeScript
The command-line TypeScript compiler can be installed using the npm package manager.
1. Install TypeScript
$ npm install -g typescript
2. Verify TypeScript
$ tsc -v
# Version 4.0.2
3. Compile TypeScript files
$ tsc helloworld.ts
# helloworld.ts => helloworld.js
Of course, for those who are just getting started with TypeScript, they can also use the online TypeScript Playground to learn new syntax or new features without installing typescript. By configuring the Target of TS Config, different compilation targets can be set to compile and generate different target codes.
The compilation target set in the example below is ES5:
1.3 Typical TypeScript workflow
As you can see, there are 3 ts files in the above figure: a.ts, b.ts and c.ts. These files will be compiled into 3 js files by the TypeScript compiler according to the configured compilation options, namely a.js, b.js and c.js. For most Web projects developed with TypeScript, we will also package the compiled js files and then deploy them.
1.4 TypeScript first experience
Create a new hello.ts file and enter the following:
function greet(person: string) {
return 'Hello, ' + person;
}
console.log(greet("TypeScript"));
Then execute the tsc hello.ts command, and then a compiled file hello.js will be generated:
"use strict";
function greet(person) {
return 'Hello, ' + person;
}
console.log(greet("TypeScript"));
Observing the output after compilation above, we find that the type information of the person parameter is erased after compilation. TypeScript will only statically check the type during the compilation phase. If an error is found, an error will be reported during compilation. At runtime, the compiled JS is the same as a normal JavaScript file, without type checking.
Two, TypeScript basic types
2.1 Boolean type
let isDone: boolean = false;
// ES5:var isDone = false;
2.2 Number type
let count: number = 10;
// ES5:var count = 10;
2.3 String type
let name: string = "semliker";
// ES5:var name = 'semlinker';
2.4 Symbol type
const sym = Symbol();
let obj = {
[sym]: "semlinker",
};
console.log(obj[sym]); // semlinker
2.5 Array type
let list: number[] = [1, 2, 3];
// ES5:var list = [1,2,3];
let list: Array<number> = [1, 2, 3]; // Array<number>泛型语法
// ES5:var list = [1,2,3];
2.6 Enum type
Using enumerations we can define some constants with names. Use enumerations to clearly express intent or create a set of differentiated use cases. TypeScript supports numeric and string-based enumerations.
1. Number enumeration
enum Direction {
NORTH,
SOUTH,
EAST,
WEST,
}
let dir: Direction = Direction.NORTH;
By default, the initial value of NORTH is 0, and the remaining members will automatically grow from 1. In other words, the value of Direction.SOUTH is 1, the value of Direction.EAST is 2, and the value of Direction.WEST is 3.
After the above enumeration example is compiled, the corresponding ES5 code is as follows:
"use strict";
var Direction;
(function (Direction) {
Direction[(Direction["NORTH"] = 0)] = "NORTH";
Direction[(Direction["SOUTH"] = 1)] = "SOUTH";
Direction[(Direction["EAST"] = 2)] = "EAST";
Direction[(Direction["WEST"] = 3)] = "WEST";
})(Direction || (Direction = {}));
var dir = Direction.NORTH;
Of course, we can also set the initial value of NORTH, such as:
enum Direction {
NORTH = 3,
SOUTH,
EAST,
WEST,
}
2. String enumeration
In TypeScript 2.4 version, we are allowed to use string enumeration. In a string enumeration, each member must be initialized with a string literal or another string enumeration member.
enum Direction {
NORTH = "NORTH",
SOUTH = "SOUTH",
EAST = "EAST",
WEST = "WEST",
}
The ES5 code corresponding to the above code is as follows:
"use strict";
var Direction;
(function (Direction) {
Direction["NORTH"] = "NORTH";
Direction["SOUTH"] = "SOUTH";
Direction["EAST"] = "EAST";
Direction["WEST"] = "WEST";
})(Direction || (Direction = {}));
By observing the compilation results of numeric enumeration and string enumeration, we can know that in addition to the ordinary mapping from member name to member value, numeric enumeration also supports reverse mapping from member value to member name:
enum Direction {
NORTH,
SOUTH,
EAST,
WEST,
}
let dirName = Direction[0]; // NORTH
let dirVal = Direction["NORTH"]; // 0
In addition, for pure string enumeration, we cannot omit any initialization procedures. If the numeric enumeration does not explicitly set the value, it will be initialized using the default rules.
3. Constant enumeration
In addition to number enumeration and string enumeration, there is a special kind of enumeration-constant enumeration. It is an enumeration decorated with the const keyword. Constant enumeration will use inline syntax and will not compile and generate any JavaScript for the enumeration type. In order to better understand this sentence, let's look at a specific example:
const enum Direction {
NORTH,
SOUTH,
EAST,
WEST,
}
let dir: Direction = Direction.NORTH;
The ES5 code corresponding to the above code is as follows:
"use strict";
var dir = 0 /* NORTH */;
4. Heterogeneous enumeration
The member values of heterogeneous enumerations are a mixture of numbers and strings:
enum Enum {
A,
B,
C = "C",
D = "D",
E = 8,
F,
}
The ES5 code for the above code is as follows:
"use strict";
var Enum;
(function (Enum) {
Enum[Enum["A"] = 0] = "A";
Enum[Enum["B"] = 1] = "B";
Enum["C"] = "C";
Enum["D"] = "D";
Enum[Enum["E"] = 8] = "E";
Enum[Enum["F"] = 9] = "F";
})(Enum || (Enum = {}));
By observing the generated ES5 code above, we can find that the number enumeration has more "reverse mapping" compared to the string enumeration:
console.log(Enum.A) //输出:0
console.log(Enum[0]) // 输出:A
2.7 Any type
In TypeScript, any type can be classified as any type. This makes the any type the top type of the type system (also known as the global super type).
let notSure: any = 666;
notSure = "semlinker";
notSure = false;
The any type is essentially an escape capsule of the type system. As developers, this gives us a lot of freedom: TypeScript allows us to perform any operation on any type of value without having to perform any form of inspection beforehand. for example:
let value: any;
value.foo.bar; // OK
value.trim(); // OK
value(); // OK
new value(); // OK
value[0][1]; // OK
In many scenarios, this is too loose. Using the any type, you can easily write code that is the correct type but has problems at runtime. If we use the any type, we cannot use the extensive protection mechanisms provided by TypeScript. In order to solve the problems caused by any, TypeScript 3.0 introduced the unknown type.
2.8 Unknown type
Just as all types can be assigned to any, all types can also be assigned to unknown. This makes unknown another top-level type of the TypeScript type system (the other is any). Let's take a look at an example of the use of the unknown type:
let value: unknown;
value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = null; // OK
value = undefined; // OK
value = new TypeError(); // OK
value = Symbol("type"); // OK
All assignments to the value variable are considered to be of the correct type. But what happens when we try to assign a value of type unknown to a variable of other type?
let value: unknown;
let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
let value5: string = value; // Error
let value6: object = value; // Error
let value7: any[] = value; // Error
let value8: Function = value; // Error
The unknown type can only be assigned to the any type and the unknown type itself. Intuitively, this makes sense: only a container that can hold any type of value can hold a value of type unknown. After all, we don't know what type of value is stored in the variable value.
Now let's see what happens when we try to perform an operation on a value of type unknown. Here is the same operation we saw in the any chapter:
let value: unknown;
value.foo.bar; // Error
value.trim(); // Error
value(); // Error
new value(); // Error
value[0][1]; // Error
After setting the value variable type to unknown, these operations are no longer considered to be type correct. By changing the any type to the unknown type, we have changed the default setting that allows all changes to prohibit any changes.
2.9 Tuple type
As we all know, arrays are generally composed of the same type of values, but sometimes we need to store different types of values in a single variable, then we can use tuples. There are no tuples in JavaScript. Tuples are unique types in TypeScript and work like arrays.
Tuples can be used to define types with a limited number of unnamed attributes. Each attribute has an associated type. When using tuples, the value of each attribute must be provided. In order to understand the concept of tuples more intuitively, let's look at a specific example:
let tupleType: [string, boolean];
tupleType = ["semlinker", true];
In the above code, we define a variable named tupleType whose type is an array of types [string, boolean], and then we initialize the tupleType variables in turn according to the correct type. As with arrays, we can access the elements of tuples through subscripts:
console.log(tupleType[0]); // semlinker
console.log(tupleType[1]); // true
When the tuple is initialized, if the type does not match, for example:
tupleType = [true, "semlinker"];
At this time, the TypeScript compiler will prompt the following error message:
[0]: Type 'true' is not assignable to type 'string'.
[1]: Type 'string' is not assignable to type 'boolean'.
Obviously it is caused by the type mismatch. When the tuple is initialized, we must also provide the value of each attribute, otherwise errors will occur, such as:
tupleType = ["semlinker"];
At this time, the TypeScript compiler will prompt the following error message:
Property '1' is missing in type '[string]' but required in type '[string, boolean]'.
2.10 Void type
To some extent, the void type is like the opposite of the any type, which means that there is no type. When a function does not return a value, you will usually see that its return value type is void:
// 声明函数返回值为void
function warnUser(): void {
console.log("This is my warning message");
}
The ES5 code generated by the above code compilation is as follows:
"use strict";
function warnUser() {
console.log("This is my warning message");
}
It should be noted that declaring a variable of type void has no effect, because in strict mode, its value can only be undefined:
let unusable: void = undefined;
2.11 Null and Undefined types
In TypeScript, undefined and null have their own types, undefined and null respectively.
let u: undefined = undefined;
let n: null = null;
2.12 object, Object and {} types
1.object type
The object type is: a new type introduced by TypeScript 2.2, which is used to represent non-primitive types.
// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
create(o: object | null): any;
// ...
}
const proto = {};
Object.create(proto); // OK
Object.create(null); // OK
Object.create(undefined); // Error
Object.create(1337); // Error
Object.create(true); // Error
Object.create("oops"); // Error
2.Object type
Object type: It is the type of all instances of the Object class, and it is defined by the following two interfaces:
The Object interface defines the properties on the Object.prototype prototype object;
// node_modules/typescript/lib/lib.es5.d.ts
interface Object {
constructor: Function;
toString(): string;
toLocaleString(): string;
valueOf(): Object;
hasOwnProperty(v: PropertyKey): boolean;
isPrototypeOf(v: Object): boolean;
propertyIsEnumerable(v: PropertyKey): boolean;
}
The ObjectConstructor interface defines the properties of the Object class.
// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
/** Invocation via `new` */
new(value?: any): Object;
/** Invocation via function calls */
(value?: any): any;
readonly prototype: Object;
getPrototypeOf(o: any): any;
// ···
}
declare var Object: ObjectConstructor;
All instances of the Object class inherit all the properties in the Object interface.
3. {}
type
The {} type describes an object with no members. When you try to access any property of such an object, TypeScript will generate a compile-time error.
// Type {}
const obj = {};
// Error: Property 'prop' does not exist on type '{}'.
obj.prop = "semlinker";
However, you can still use all the properties and methods defined on the Object type. These properties and methods can be used implicitly through the JavaScript prototype chain:
// Type {}
const obj = {};
// "[object Object]"
obj.toString();
2.13 Never type
The never type represents the type of values that never exist. For example, the never type is the return value type of a function expression or arrow function expression that always throws an exception or does not return a value at all.
// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {}
}
In TypeScript, you can use the characteristics of the never type to achieve comprehensive checks. 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, which is narrowed down to never, to a display-declared never variable. If everything is logically correct, then it should be able to compile and pass. But if one day later, your colleague changes the type of Foo:
type Foo = string | number | boolean;
However, he forgot to modify the control flow in the controlFlowAnalysisWithNever method at the same time. At this time, the foo type of the else branch will be narrowed to the boolean type, which makes it impossible to assign to the never type, and a compilation error will occur. In this way, we can ensure
The controlFlowAnalysisWithNever method always exhausts all possible types of Foo. Through this example, we can draw a conclusion: use never to avoid the emergence of new joint types without corresponding implementation, the purpose is to write code that is absolutely type safe.
Three, TypeScript assertion
3.1 Type assertion
Sometimes you will encounter such a situation, you will know the details of a value better than TypeScript. Usually this happens when you clearly know that an entity has a more exact type than its existing type.
Through type assertion, you can tell the compiler, "Trust me, I know what I'm doing." Type assertion is like type conversion in other languages, but without special data checking and deconstruction. It has no runtime impact, but only works during the compilation phase.
There are two forms of type assertion:
1. "Angle Brackets" Syntax
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
2.as syntax
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
3.2 non-empty assertion
In the context when the type checker cannot determine the type, a new postfix expression operator! Can be used to assert that the operation object is of a non-null and non-undefined type. Specifically, x! will exclude null and undefined from the x range.
So what is the use of the non-empty assertion operator? Let's first look at some usage scenarios of non-empty assertion operators.
1. Ignore undefined and null types
function myFunc(maybeString: string | undefined | null) {
// Type 'string | null | undefined' is not assignable to type 'string'.
// Type 'undefined' is not assignable to type 'string'.
const onlyString: string = maybeString; // Error
const ignoreUndefinedAndNull: string = maybeString!; // Ok
}
2. Ignore the undefined type when calling the function
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
}
Because the! Non-empty assertion operator will be removed from the compiled JavaScript code, you must pay special attention to it during actual use. For example, the following example:
const a: number | undefined = undefined;
const b: number = a!;
console.log(b);
The above TS code will compile and generate the following ES5 code:
"use strict";
const a = undefined;
const b = a;
console.log(b);
Although in the TS code, we use non-empty assertions, so that the const b: number = a!; statement can pass the TypeScript type checker. But in the generated ES5 code, the! Non-empty assertion operator has been removed, so if the above code is executed in the browser, undefined will be output in the console.
3.3 Determine the assignment assertion
In TypeScript version 2.7, a deterministic assignment assertion was introduced, that is, it is allowed to place a! Sign after the instance attribute and variable declaration to tell TypeScript that the attribute will be assigned explicitly. In order to better understand its role, let's look at a specific 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 is that the variable x was used before the assignment. To solve this problem, we can use the deterministic assignment assertion:
let x!: number;
initialize();
console.log(2 * x); // Ok
function initialize() {
x = 10;
}
By let x!: number; determine the assignment assertion, the TypeScript compiler will know that the attribute will be assigned explicitly.
Four, type guard
Type protection is an expression that can be checked at runtime to ensure that the type is within a certain range. In other words, type protection can guarantee that a string is a string, although its value can also be a numeric value. Type protection is not completely different from feature detection. The main idea is to try to detect attributes, methods, or prototypes to determine how to handle values. There are currently four main ways to implement type protection:
4.1 in keyword
interface Admin {
name: string;
privileges: string[];
}
interface Employee {
name: string;
startDate: Date;
}
type UnknownEmployee = Employee | Admin;
function printEmployeeInformation(emp: UnknownEmployee) {
console.log("Name: " + emp.name);
if ("privileges" in emp) {
console.log("Privileges: " + emp.privileges);
}
if ("startDate" in emp) {
console.log("Start Date: " + emp.startDate);
}
}
4.2 typeof keyword
function padLeft(value: string, padding: string | number) {
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}'.`);
}
Typeof type protection only supports two forms: typeof v === "typename" and typeof v !== typename, "typename" must be "number", "string", "boolean" or "symbol". But TypeScript does not prevent you from comparing with other strings, and the language does not recognize those expressions as type protection.
4.3 instanceof keyword
interface Padder {
getPaddingString(): string;
}
class SpaceRepeatingPadder implements Padder {
constructor(private numSpaces: number) {}
getPaddingString() {
return Array(this.numSpaces + 1).join(" ");
}
}
class StringPadder implements Padder {
constructor(private value: string) {}
getPaddingString() {
return this.value;
}
}
let padder: Padder = new SpaceRepeatingPadder(6);
if (padder instanceof SpaceRepeatingPadder) {
// padder的类型收窄为 'SpaceRepeatingPadder'
}
4.4 Type predicates protected by custom types
function isNumber(x: any): x is number {
return typeof x === "number";
}
function isString(x: any): x is string {
return typeof x === "string";
}
Five, union type and type alias
5.1 Union type
Union types are usually used with null or undefined:
const sayHello = (name: string | undefined) => {
/* ... */
};
For example, here the type of name is string | undefined means that the value of string or undefined can be passed to the sayHello function.
sayHello("semlinker");
sayHello(undefined);
Through this example, you can intuitively know that the combined type of type A and type B is a type that accepts both A and B values. In addition, for union types, you may encounter the following usage:
let num: 1 | 2 = 1;
type EventNames = 'click' | 'scroll' | 'mousemove';
The 1, 2 or'click' in the above example is called a literal type, which is used to restrict the value to only one of a few values.
5.2 Recognizable union
TypeScript can identify the type of union (Discriminated Unions), also known as algebraic data type or label union type. It contains 3 main points: recognizable, joint type, and type guard.
The essence of this type is a type protection method that combines combined type and literal type. If a type is a combined type of multiple types, and multiple types have a common attribute, then this common attribute can be used to create different type protection blocks.
1. Recognizable
Recognizable requires that each element in the union type contains a singleton type attribute, such as:
enum CarTransmission {
Automatic = 200,
Manual = 300
}
interface Motorcycle {
vType: "motorcycle"; // discriminant
make: number; // year
}
interface Car {
vType: "car"; // discriminant
transmission: CarTransmission
}
interface Truck {
vType: "truck"; // discriminant
capacity: number; // in tons
}
In the above code, we have defined three interfaces Motorcycle, Car and Truck respectively. These interfaces all contain a vType attribute. This attribute is called an identifiable attribute, while other attributes are only related to the characteristic interface.
2. Joint type
Based on the three interfaces defined above, we can create a Vehicle joint type:
type Vehicle = Motorcycle | Car | Truck;
Now we can start to use the Vehicle joint type. For the Vehicle type variable, it can represent different types of vehicles.
3. Type guard
Let's define an evaluatePrice method, which is used to calculate the price according to the type, capacity and evaluation factor of the vehicle. The specific implementation is as follows:
const EVALUATION_FACTOR = Math.PI;
function evaluatePrice(vehicle: Vehicle) {
return vehicle.capacity * EVALUATION_FACTOR;
}
const myTruck: Truck = { vType: "truck", capacity: 9.5 };
evaluatePrice(myTruck);
For the above code, the TypeScript compiler will prompt the following error message:
Property 'capacity' does not exist on type 'Vehicle'.
Property 'capacity' does not exist on type 'Motorcycle'.
The reason is that in the Motorcycle interface, there is no capacity property, and for the Car interface, it does not have a capacity property. So, how should we solve the above problems now? At this time, we can use type guards. Let's refactor the evaluatePrice method defined earlier. The refactored code is as follows:
function evaluatePrice(vehicle: Vehicle) {
switch(vehicle.vType) {
case "car":
return vehicle.transmission * EVALUATION_FACTOR;
case "truck":
return vehicle.capacity * EVALUATION_FACTOR;
case "motorcycle":
return vehicle.make * EVALUATION_FACTOR;
}
}
In the above code, we use switch and case operators to implement type guards to ensure that in the evaluatePrice method, we can safely access the attributes contained in the vehicle object to correctly calculate the price corresponding to the vehicle type.
5.3 Type alias
Type aliases are used to give a new name to a type.
type Message = string | string[];
let greet = (message: Message) => {
// ...
};
Six, cross type
Cross type in TypeScript is to merge multiple types into one type. With the & operator, multiple existing types can be superimposed into one type, which contains all the required characteristics.
type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };
let point: Point = {
x: 1,
y: 1
}
In the above code, we first define the PartialPointX type, then use the & operator to create a new Point type that represents a point with x and y coordinates, and then define a Point type variable and initialize it.
6.1 Combination of basic type attributes with the same name
So now the problem is here. Suppose that in the process of merging multiple types, it happens that some types have the same members, but the corresponding types are inconsistent, such as:
interface X {
c: string;
d: string;
}
interface Y {
c: number;
e: string
}
type XY = X & Y;
type YX = Y & X;
let p: XY;
let q: YX;
In the above code, both interface X and interface Y contain the same member c, but their types are inconsistent. In this case, can the type of member c in XY type or YX type be string or number type at this time? For example, the following example:
p = { c: 6, d: "d", e: "e" };
q = { c: "c", d: "d", e: "e" };
Why does the type of member c become never after interface X and interface Y are mixed in? This is because the type of member c after mixing is string & number, that is, the type of member c can be either string type or number type. Obviously this type does not exist, so the type of member c after mixing is never.
6.2 Combination of non-basic type attributes with the same name
In the above example, it happens that the types of the internal member c in interface X and interface Y are both basic data types, so what will happen if they are non-basic data types. Let's look at a specific example:
interface D { d: boolean; }
interface E { e: string; }
interface F { f: number; }
interface A { x: D; }
interface B { x: E; }
interface C { x: F; }
type ABC = A & B & C;
let abc: ABC = {
x: {
d: true,
e: 'semlinker',
f: 666
}
};
console.log('abc:', abc);
After the above code runs successfully, the console will output the following results:
As can be seen from the above figure, when multiple types are mixed, if there are the same members, and the member type is a non-basic data type, then it can be merged successfully.
Seven, TypeScript functions
7.1 The difference between TypeScript function and JavaScript function
TypeScript | JavaScript |
---|---|
Containing type | No type |
Arrow function | Arrow function (ES2015) |
Function type | No function type |
Required and optional parameters | All parameters are optional |
Default parameters | Default parameters |
Remaining parameters | Remaining parameters |
Function overloading | No function overloading |
7.2 Arrow functions
1. Common grammar
myBooks.forEach(() => console.log('reading'));
myBooks.forEach(title => console.log(title));
myBooks.forEach((title, idx, arr) =>
console.log(idx + '-' + title);
);
myBooks.forEach((title, idx, arr) => {
console.log(idx + '-' + title);
});
2. Example of use
// 未使用箭头函数
function Book() {
let self = this;
self.publishDate = 2016;
setInterval(function () {
console.log(self.publishDate);
}, 1000);
}
// 使用箭头函数
function Book() {
this.publishDate = 2016;
setInterval(() => {
console.log(this.publishDate);
}, 1000);
}
7.3 Parameter types and return types
function createUserId(name: string, id: number): string {
return name + id;
}
7.4 Function types
let IdGenerator: (chars: string, nums: number) => string;
function createUserId(name: string, id: number): string {
return name + id;
}
IdGenerator = createUserId;
7.5 Optional parameters and default parameters
// 可选参数
function createUserId(name: string, id: number, age?: number): string {
return name + id;
}
// 默认参数
function createUserId(
name = "semlinker",
id: number,
age?: number
): string {
return name + id;
}
When declaring a function, you can use? To define optional parameters, such as age?: number. In actual use, it should be noted that optional parameters should be placed after ordinary parameters, otherwise it will cause compilation errors.
7.6 Remaining parameters
function push(array, ...items) {
items.forEach(function (item) {
array.push(item);
});
}
let a = [];
push(a, 1, 2, 3);
7.7 Function overloading
Function overloading or method overloading is the ability to create multiple methods with the same name and different numbers or types of parameters.
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: Combinable, b: Combinable) {
// type Combinable = string | number;
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
In the above code, we provide multiple function type definitions for the add function to implement function overloading. In addition to overloading ordinary functions in TypeScript, we can also overload member methods in a class.
Method overloading refers to the method with the same name and different parameters in the same class (different parameter types, different numbers of parameters, or the same number of parameters in the order of the parameters). When calling, select the one that matches it according to the form of the actual parameter. A technique for performing operations. Therefore, the condition for member methods in a class to meet the overload is: in the same class, the method name is the same and the parameter list is different. Let's give an example of member method overloading:
class Calculator {
add(a: number, b: number): number;
add(a: string, b: string): string;
add(a: string, b: number): string;
add(a: number, b: string): string;
add(a: Combinable, b: Combinable) {
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
}
const calculator = new Calculator();
const result = calculator.add('Semlinker', ' Kakuqo');
It should be noted here that when the TypeScript compiler processes a function overload, it will look up the overload list and try to use the first overload definition. Use this if it matches. Therefore, when defining overloads, be sure to put the most precise definition first. In addition, in the Calculator class, add(a: Combinable, b: Combinable){} is not part of the overload list, so for the add member method, we only define four overload methods.
8. TypeScript array
8.1 Array destructuring
let x: number; let y: number; let z: number;
let five_array = [0,1,2,3,4];
[x,y,z] = five_array;
8.2 Array expansion operator
let two_array = [0, 1];
let five_array = [...two_array, 2, 3, 4];
8.3 Array Traversal
let colors: string[] = ["red", "green", "blue"];
for (let i of colors) {
console.log(i);
}
Nine, TypeScript objects
9.1 Object Deconstruction
let person = {
name: "Semlinker",
gender: "Male",
};
let { name, gender } = person;
9.2 Object expansion operator
let person = {
name: "Semlinker",
gender: "Male",
address: "Xiamen",
};
// 组装对象
let personWithAge = { ...person, age: 33 };
// 获取除了某些项外的其它项
let { name, ...rest } = person;
Ten, TypeScript interface
In object-oriented languages, interface is a very important concept, it is an abstraction of behavior, and how to act specifically needs to be implemented by the class.
The interface in TypeScript is a very flexible concept. In addition to abstracting part of the behavior of a class, it is also often used to describe the "shape of an object".
10.1 Object shape
interface Person {
name: string;
age: number;
}
let semlinker: Person = {
name: "semlinker",
age: 33,
};
10.2 Optional | Read-only attribute
interface Person {
readonly name: string;
age?: number;
}
The read-only attribute is used to limit the value of the object can only be modified when the object is just created. In addition, TypeScript also provides the ReadonlyArray<T> type, which is similar to Array<T>, except that all the 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!
10.3 Arbitrary attributes
Sometimes we hope that in addition to mandatory and optional attributes, an interface also allows other arbitrary attributes, then we can use the form of index signature to meet the above requirements.
interface Person {
name: string;
age?: number;
[propName: string]: any;
}
const p1 = { name: "semlinker" };
const p2 = { name: "lolo", age: 5 };
const p3 = { name: "kakuqo", sex: 1 }
10.4 The difference between interface and type alias
1.Objects/Functions
Both interface and type aliases can be used to describe the shape or function signature of an object:
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;
2.Other Types
Unlike interface types, type aliases can be used for some other types, such as primitive types, union types, and tuples:
// primitive
type Name = string;
// object
type PartialPointX = { x: number; };
type PartialPointY = { y: number; };
// union
type PartialPoint = PartialPointX | PartialPointY;
// tuple
type Data = [number, string];
3.Extend
Both interfaces and type aliases can be extended, but the syntax is different. In addition, interfaces and type aliases are not mutually exclusive. Interfaces can extend type aliases, but not the other way around.
Interface extends interface
interface PartialPointX { x: number; }
interface Point extends PartialPointX {
y: number;
}
Type alias extends type alias
type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };
Interface extends type alias
type PartialPointX = { x: number; };
interface Point extends PartialPointX { y: number; }
Type alias extends interface
interface PartialPointX { x: number; }
type Point = PartialPointX & { y: number; };
4.Implements
Classes can implement interfaces or type aliases in the same way, but classes cannot implement union types defined using type aliases:
interface Point {
x: number;
y: number;
}
class SomePoint implements Point {
x = 1;
y = 2;
}
type Point2 = {
x: number;
y: number;
};
class SomePoint2 implements Point2 {
x = 1;
y = 2;
}
type PartialPoint = { x: number; } | { y: number; };
// A class can only implement an object type or
// intersection of object types with statically known members.
class SomePartialPoint implements PartialPoint { // Error
x = 1;
y = 2;
}
5.Declaration merging
Unlike type aliases, interfaces can be defined multiple times and will be automatically merged into a single interface.
interface Point { x: number; }
interface Point { y: number; }
const point: Point = { x: 1, y: 2 };
11. TypeScript class
11.1 Class attributes and methods
In an object-oriented language, a class is a construction of an object-oriented computer programming language, a blueprint for creating objects, and describing the common attributes and methods of the created objects.
In TypeScript, we can define a class through the Class keyword:
class Greeter {
// 静态属性
static cname: string = "Greeter";
// 成员属性
greeting: string;
// 构造函数 - 执行初始化操作
constructor(message: string) {
this.greeting = message;
}
// 静态方法
static getClassName() {
return "Class name is Greeter";
}
// 成员方法
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");
So what is the difference between member properties and static properties, and member methods and static methods? There is no need to explain too much here, let's take a look directly at the ES5 code generated by the compilation:
"use strict";
var Greeter = /** @class */ (function () {
// 构造函数 - 执行初始化操作
function Greeter(message) {
this.greeting = message;
}
// 静态方法
Greeter.getClassName = function () {
return "Class name is Greeter";
};
// 成员方法
Greeter.prototype.greet = function () {
return "Hello, " + this.greeting;
};
// 静态属性
Greeter.cname = "Greeter";
return Greeter;
}());
var greeter = new Greeter("world");
11.2 ECMAScript private fields
ECMAScript private fields have been supported since TypeScript 3.8, and they can be used as follows:
class Person {
#name: string;
constructor(name: string) {
this.#name = name;
}
greet() {
console.log(`Hello, my name is ${this.#name}!`);
}
}
let semlinker = new Person("Semlinker");
semlinker.#name;
// ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.
Unlike regular properties (even properties declared with the private modifier), private fields need to keep the following rules in mind:
Private fields start with the # character, sometimes we call them private names;
Each private field name is uniquely limited to the class it contains;
You cannot use TypeScript accessibility modifiers (such as public or private) on private fields;
Private fields cannot be accessed outside of the containing class, and cannot even be detected.
11.3 Accessor
In TypeScript, we can implement data encapsulation and validity verification through getter and setter methods to prevent abnormal data.
let passcode = "Hello TypeScript";
class Employee {
private _fullName: string;
get fullName(): string {
return this._fullName;
}
set fullName(newName: string) {
if (passcode && passcode == "Hello TypeScript") {
this._fullName = newName;
} else {
console.log("Error: Unauthorized update of employee!");
}
}
}
let employee = new Employee();
employee.fullName = "Semlinker";
if (employee.fullName) {
console.log(employee.fullName);
}
11.4 Class inheritance
Inheritance is a hierarchical model of connecting classes and classes. Refers to the ability of a class (called subclass, subinterface) to inherit the functions of another class (called parent class, parent interface) and can add its own new functions. Inheritance refers to the class and class or interface and The most common relationship between interfaces.
Inheritance is an is-a relationship:
In TypeScript, we can implement inheritance through the extends keyword:
class Animal {
name: string;
constructor(theName: string) {
this.name = theName;
}
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Snake extends Animal {
constructor(name: string) {
super(name); // 调用父类的构造函数
}
move(distanceInMeters = 5) {
console.log("Slithering...");
super.move(distanceInMeters);
}
}
let sam = new Snake("Sammy the Python");
sam.move();
11.5 Abstract class
Classes declared with the abstract keyword are called abstract classes. An abstract class cannot be instantiated because it contains one or more abstract methods. The so-called abstract method refers to a method that does not contain specific implementation:
abstract class Person {
constructor(public name: string){}
abstract say(words: string) :void;
}
// Cannot create an instance of an abstract class.(2511)
const lolo = new Person(); // Error
Abstract classes cannot be instantiated directly, we can only instantiate subclasses that implement all abstract methods. The details are as follows:
abstract class Person {
constructor(public name: string){}
// 抽象方法
abstract say(words: string) :void;
}
class Developer extends Person {
constructor(name: string) {
super(name);
}
say(words: string): void {
console.log(`${this.name} says ${words}`);
}
}
const lolo = new Developer("lolo");
lolo.say("I love ts!"); // lolo says I love ts!
11.6 Class method overloading
In the previous chapters, we have introduced function overloading. For class methods, it also supports overloading. For example, in the following example, we overload the getProducts member method of the ProductService class:
class ProductService {
getProducts(): void;
getProducts(id: number): void;
getProducts(id?: number) {
if(typeof id === 'number') {
console.log(`获取id为 ${id} 的产品信息`);
} else {
console.log(`获取所有的产品信息`);
}
}
}
const productService = new ProductService();
productService.getProducts(666); // 获取id为 666 的产品信息
productService.getProducts(); // 获取所有的产品信息
12. TypeScript generics
In software engineering, we must not only create a consistent and well-defined API, but also consider reusability. Components can not only support current data types, but also future data types, which provides you with very flexible functions when creating large-scale systems.
In languages like C# and Java, generics can be used to create reusable components, and one component can support multiple types of data. In this way, users can use components with their own data types.
The key purpose of designing generics is to provide meaningful constraints between members. These members can be: class instance members, class methods, function parameters, and function return values.
Generics is a template that allows the same function to accept different types of parameters. Rather than using any type, it is better to use generics to create reusable components, because generics retain parameter types.
12.1 Generic syntax
For readers who are new to TypeScript generics, the syntax of <T> will be unfamiliar for the first time. In fact, it is nothing special, just like passing parameters, we pass the type we want to use for a specific function call.
Refer to the picture above, when we call identity<Number>(1), the Number type is just like the parameter 1, and it will be filled with this type wherever T appears. The T inside <T> in the figure is called a type variable, which is the type placeholder we want to pass to the identity function, and it is assigned to the value parameter to replace its type: at this time, T acts as a type, Rather than the specific Number type.
Where T stands for Type, which is usually used as the first type variable name when defining generics. But actually T can be replaced by any valid name. In addition to T, the following are the meanings of common generic variables:
K (Key): indicates the type of key in the object;
V (Value): represents the value type in the object;
E (Element): Represents the element type.
In fact, it is not only possible to define one type variable, we can introduce any number of type variables we wish to define. For example, we introduce a new type variable U to extend the identity function we defined:
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}
console.log(identity<Number, string>(68, "Semlinker"));
In addition to explicitly setting values for type variables, a more common practice is to make the compiler automatically select these types, thereby making the code more concise. We can omit the angle brackets altogether, such as:
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 our parameter types and assign them to T and U without requiring the developer to specify them explicitly.
12.2 Generic interface
interface GenericIdentityFn<T> {
(arg: T): T;
}
12.3 Generic classes
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
return x + y;
};
12.4 Generic tool types
For the convenience of developers, TypeScript has built-in some commonly used tool types, such as Partial, Required, Readonly, Record, and ReturnType. For space considerations, here we only briefly introduce Partial tool types. But before the specific introduction, we have to introduce some relevant basic knowledge, so that readers can learn other types of tools by themselves.
1.typeof
In TypeScript, the typeof operator can be used to obtain a variable declaration or the type of an object.
interface Person {
name: string;
age: number;
}
const sem: Person = { name: 'semlinker', age: 33 };
type Sem= typeof sem; // -> Person
function toArray(x: number): Array<number> {
return [x];
}
type Func = typeof toArray; // -> (x: number) => number[]
2.keyof
The keyof operator was introduced in TypeScript 2.1. This operator can be used to get all 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, digital index and string index:
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, 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 into a string index when performing an index operation. So the result of keyof {[x: string]: Person} will return string | number.
3.in
in is used to traverse enumeration types:
type Keys = "a" | "b" | "c"
type Obj = {
[p in Keys]: any
} // -> { a: any, b: any, c: any }
4.infer
In the conditional type statement, you can use infer to declare a type variable and use it.
type ReturnType<T> = T extends (
...args: any[]
) => infer R ? R : any;
Infer R in the above code is to declare a variable to carry the return value type of the passed function signature. Simply put, it is used to get the type of the return value of the function 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 this generic function is defined with constraints, so it is no longer applicable to any type:
loggingIdentity(3); // Error, number doesn't have a .length property
At this time, we need to pass in a value that meets the constraint type, which must contain the necessary attributes:
loggingIdentity({length: 10, value: 3});
6.Partial
The function of Partial<T> is to make all the attributes in a certain type optional ?.
definition:
/**
* node_modules/typescript/lib/lib.es5.d.ts
* Make all properties in T optional
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};
In the above code, first get all the attribute names of T through keyof T, then use in to traverse, assign the value to P, and finally get the corresponding attribute value through T[P]. The? Sign in the middle is used to make all attributes optional.
Example:
interface Todo {
title: string;
description: string;
}
function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
return { ...todo, ...fieldsToUpdate };
}
const todo1 = {
title: "Learn TS",
description: "Learn TypeScript",
};
const todo2 = updateTodo(todo1, {
description: "Learn TypeScript Enum",
});
In the updateTodo method above, we use the Partial<T> tool type to define the type of fieldsToUpdate as Partial<Todo>, namely:
{
title?: string | undefined;
description?: string | undefined;
}
13. TypeScript decorator
13.1 What is a decorator
It is an expression
After the expression is executed, it returns a function
The input parameters of the function are target, name and descriptor respectively
After the function is executed, a descriptor object may be returned, which is used to configure the target object
13.2 Classification of decorators
Class decorators
Property decorators
Method decorators
Parameter decorators
It should be noted that to enable the experimental decorator feature, you must enable the experimentalDecorators compiler option in the command line or tsconfig.json:
Command Line:
tsc --target ES5 --experimentalDecorators
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
13.3 Class decorators
Class decorator declaration:
declare type ClassDecorator = <TFunction extends Function>(
target: TFunction
) => TFunction | void;
As the name suggests, class decorators are used to decorate classes. It receives one parameter:
target: TFunction-the decorated class
After seeing the first glance, do you feel bad? It's okay, let's take an example right away:
function Greeter(target: Function): void {
target.prototype.greet = function (): void {
console.log("Hello Semlinker!");
};
}
@Greeter
class Greeting {
constructor() {
// 内部实现
}
}
let myGreeting = new Greeting();
(myGreeting as any).greet(); // console output: 'Hello Semlinker!';
In the above example, we defined the Greeter class decorator, and we used @Greeter syntactic sugar to use the decorator.
Friendly reminder: readers can directly copy the above code and run it in TypeScript Playground to view the results.
Some readers may want to ask, the example always outputs Hello Semlinker!, can I customize the output greeting? This question is very good, the answer is yes.
The specific implementation is as follows:
function Greeter(greeting: string) {
return function (target: Function) {
target.prototype.greet = function (): void {
console.log(greeting);
};
};
}
@Greeter("Hello TS!")
class Greeting {
constructor() {
// 内部实现
}
}
let myGreeting = new Greeting();
(myGreeting as any).greet(); // console output: 'Hello TS!';
13.4 Attribute Decorator
Attribute decorator declaration:
declare type PropertyDecorator = (target:Object,
propertyKey: string | symbol ) => void;
As the name suggests, the attribute decorator is used to decorate the attributes of a class. It receives two parameters:
target: Object-the decorated class
propertyKey: string | symbol-the property name of the decorated class
Strike while the iron is hot, and immediately warm up with an example:
function logProperty(target: any, key: string) {
delete target[key];
const backingField = "_" + key;
Object.defineProperty(target, backingField, {
writable: true,
enumerable: true,
configurable: true
});
// property getter
const getter = function (this: any) {
const currVal = this[backingField];
console.log(`Get: ${key} => ${currVal}`);
return currVal;
};
// property setter
const setter = function (this: any, newVal: any) {
console.log(`Set: ${key} => ${newVal}`);
this[backingField] = newVal;
};
// Create new property with getter and setter
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
class Person {
@logProperty
public name: string;
constructor(name : string) {
this.name = name;
}
}
const p1 = new Person("semlinker");
p1.name = "kakuqo";
In the above code, we define a logProperty function to track the user's operation on the property. When the code runs successfully, the following result will be output in the console:
Set: name => semlinker
Set: name => kakuqo
13.5 Method decorator
Method decorator declaration:
declare type MethodDecorator = <T>(target:Object, propertyKey: string | symbol,
descriptor: TypePropertyDescript<T>) => TypedPropertyDescriptor<T> | void;
As the name suggests, method decorators are used to decorate methods of a class. It receives three parameters:
target: Object-the decorated class
propertyKey: string | symbol-method name
descriptor: TypePropertyDescript-Property descriptor
Don't talk nonsense, just go to the example:
function log(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
let originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log("wrapped function: before invoking " + propertyKey);
let result = originalMethod.apply(this, args);
console.log("wrapped function: after invoking " + propertyKey);
return result;
};
}
class Task {
@log
runTask(arg: any): any {
console.log("runTask invoked, args: " + arg);
return "finished";
}
}
let task = new Task();
let result = task.runTask("learn ts");
console.log("result: " + result);
After the above code runs successfully, the console will output the following results:
"wrapped function: before invoking runTask"
"runTask invoked, args: learn ts"
"wrapped function: after invoking runTask"
"result: finished"
Let's introduce the parameter decorator.
13.6 Parameter decorator
Parameter decorator declaration:
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol,
parameterIndex: number ) => void
As the name suggests, the parameter decorator is used to decorate function parameters. It receives three parameters:
target: Object-the decorated class
propertyKey: string | symbol-method name
parameterIndex: number-the index value of the parameter in the method
function Log(target: Function, key: string, parameterIndex: number) {
let functionLogged = key || target.prototype.constructor.name;
console.log(`The parameter in position ${parameterIndex} at ${functionLogged} has
been decorated`);
}
class Greeter {
greeting: string;
constructor(@Log phrase: string) {
this.greeting = phrase;
}
}
After the above code runs successfully, the console will output the following results:
"The parameter in position 0 at Greeter has been decorated"
14. New Features of TypeScript 4.0
TypeScript 4.0 brings a lot of new features, here we only briefly introduce two of the new features.
14.1 Class attribute inference of constructor
When the noImplicitAny configuration attribute is enabled, TypeScript 4.0 can use control flow analysis to confirm the attribute type in the class:
class Person {
fullName; // (property) Person.fullName: string
firstName; // (property) Person.firstName: string
lastName; // (property) Person.lastName: string
constructor(fullName: string) {
this.fullName = fullName;
this.firstName = fullName.split(" ")[0];
this.lastName = fullName.split(" ")[1];
}
}
However, for the above code, if it is in a version prior to TypeScript 4.0, such as version 3.9.2, the compiler will prompt the following error message:
class Person {
// Member 'fullName' implicitly has an 'any' type.(7008)
fullName; // Error
firstName; // Error
lastName; // Error
constructor(fullName: string) {
this.fullName = fullName;
this.firstName = fullName.split(" ")[0];
this.lastName = fullName.split(" ")[1];
}
}
Inferring the type of the class attribute from the constructor, this feature brings us convenience. But in the process of use, if we can't guarantee that all member properties are assigned, then the property may be considered undefined.
class Person {
fullName; // (property) Person.fullName: string
firstName; // (property) Person.firstName: string | undefined
lastName; // (property) Person.lastName: string | undefined
constructor(fullName: string) {
this.fullName = fullName;
if(Math.random()){
this.firstName = fullName.split(" ")[0];
this.lastName = fullName.split(" ")[1];
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。