💡 入门教程推荐:[https://ts.xcatliu.com/](https://ts.xcatliu.com/)

本文不是 TS 的入门教程,可以看成是 《TypeScript 入门教程》的读书笔记,在后面使用 TS 的过程中,想不起来怎么用时,可以用来查询和翻阅的工具。

原始数据类型

JS 中包含的原始数据类型有:布尔值、数值、字符串、nullundefined 以及 ES6 中的新类型 Symbol 和 ES10 中的新类型 BigInt

下面介绍下这些原始数据类型在 TS 中的应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 布尔值
const isDone: boolean = false;

// 数值
const decLiteral: number = 6;

// 字符串
const myName: string = 'Tom';

// Null 和 Undefined
const u: undefined = undefined;
const n: null = null;

// Symbol
const s: symbol = Symbol('a');

// BigInt
const b: bigint = BigInt(10);

```</div>color3
注意:上面的用法是个错误❌例子,只是为了说明怎么显示的定义类型而作的说明,在真实的项目中不要这么使用。</div>

原因是 <font style="color:rgb(24, 32, 38);">TypeScript 能够根据参数、属性和变量的默认值或初始值推断其类型。无需显示的注释其类型。这样做会给代码增加不必要的冗长,使其更难以阅读,并且在某些情况下可能会阻止 TypeScript 推断更具体的类型。</font>

<font style="color:rgb(24, 32, 38);"></font>

<font style="color:rgb(24, 32, 38);">例如:</font>**<font style="color:rgb(24, 32, 38);">const isDone = false</font>**<font style="color:rgb(24, 32, 38);"> 的类型会自动推断为 </font>**<font style="color:rgb(24, 32, 38);">false</font>**<font style="color:rgb(24, 32, 38);">,</font>**<font style="color:rgb(24, 32, 38);">let isDone = false</font>**<font style="color:rgb(24, 32, 38);"> 的类型才会推断为 </font>**<font style="color:rgb(24, 32, 38);">boolean</font>**<font style="color:rgb(24, 32, 38);">,因此如果给 const 定义的 isDone 显示的注释其类型为 boolean 的化,会扩大它的类型范围,带来一些不必要的风险。</font>

<font style="color:rgb(24, 32, 38);"></font>

<font style="color:rgb(24, 32, 38);">因此只建议在定义一个变量没有初始值的情况下显示的定义其类型(</font><font style="color:#ED740C;">而且也是必须的</font><font style="color:rgb(24, 32, 38);">):</font></div>color3
<font style="color:rgb(51, 51, 51);">如果定义的时候没有赋值或者指定类型,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查</font></div>

```typescript
let myFavoriteNumber; // myFavoriteNumber 的类型就是 any 了,之后可以被任何类型的变量赋值
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

在定义变量没有默认值的情况下,必须指定其类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 布尔值
let isDone: boolean;

// 数值
let decLiteral: number;

// 字符串
let myName: string;

// Null 和 Undefined
let u: undefined;
let n: null;

// Symbol
let s: symbol;

// BigInt
let b: bigint;

注意,TypeScript 中使用字面量和构造函数定义的变量的类型是不一样的:

1
2
let bool1: boolean = false;
let bool2: Boolean = new Boolean(0);

其他的基本类型都是一样,不再赘述。

void 和 never

Javascript 中没有 void 和 never 的概念。在 TypeScript 中,其作用如下:

  • void表示没有任何返回值的函数
  • never表示那些永远不存的值的类型,例如那些总是会抛出异常或根本就不会有返回值的函数

void 应用场景:

1
2
3
function alertName(): void {
alert("My name is Tom");
}

声明一个 void 类型的变量没有什么用,因为你只能将它赋值为 undefinednull(只在 –strictNullChecks 未指定时)

1
let unusable: void = undefined;

never 应用场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
throw new Error(message);
}

// 推断的返回值类型为never
function fail() {
return error("Something failed");
}

// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
while (true) {}
}

never 类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是 never 的子类型或可以赋值给 never 类型(除了 never 本身之外)。 即使 any 也不可以赋值给 never。

对象类型

使用 interface 定义对象的类型:

1
2
3
4
5
6
7
8
9
interface Person {
name: string;
age: number;
}

let tom: Person = {
name: "Tom",
age: 25,
};

上面的例子中,tom 的形状和接口 Person 必须一致,对象的元素不允许多也不允许少,赋值的时候变量的形状必须和接口的形状保持一致。

如何少一些字段:

使用可选属性 ?:,但是依旧不允许添加未定义的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Person {
name: string;
age?: number;
}

let tom: Person = {
name: "Tom",
};

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

如何多一些字段:

可以定义一个任意属性,需要注意的是,一个接口只能定义一个任意属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface Person {
name: string;
age?: number;
[propName: string]: string;
}

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

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

注意:一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集上例中,任意属性的值允许是 string,但是可选属性 age 的值却是 numbernumber 不是 string 的子属性,所以报错了。

属性只读:

使用 readonly 定义的只读属性只能在创建的是否被赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Person {
readonly id: number;
name: string;
age?: number;
[propName: string]: any;
}

let tom: Person = {
id: 89757,
name: "Tom",
gender: "male",
};

tom.id = 9527;

// index.ts(14,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.

注意,只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候

数组类型

在 TypeScript 中,定义数组类型的方式有多种,比较灵活。

1
2
3
4
5
6
7
8
9
10
11
// 「类型 + 方括号」表示法
let fibonacci: number[] = [1, 1, 2, 3, 5];

// 数组泛型
let fibonacci: Array<number> = [1, 1, 2, 3, 5];

// 接口
interface NumberArray {
[index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];

函数类型

在 JavaScript 中,有两种常见的定义函数的方式 - 函数声明和函数表达式:

函数函数声明:

1
2
3
4
5
6
7
8
// 函数声明
function sum(x, y) {
return x + y;
}
// 添加类型定义
function sum(x: number, y: number): number {
return x + y;
}

函数表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 函数表达式
let mySum = function (x, y) {
return x + y;
};

// 添加类型定义
let mySum = function (x: number, y: number): number {
return x + y;
};

// 上面 mySum 的类型是根据 = 的右侧推断出来的,如果需要显示的指定其类型
let mySum: (x: number, y: number) => number = function (
x: number,
y: number
): number {
return x + y;
};

// 可以使用接口来定义函数的类型
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc = function (source: string, subString: string) {
return source.search(subString) !== -1;
};

参数可选:

与接口中的可选属性类似,我们用 ? 表示可选的参数,需要注意的是:可选参数后面不允许再出现必需参数了

1
2
3
4
5
6
7
8
9
function buildName(firstName: string, lastName?: string) {
if (lastName) {
return firstName + " " + lastName;
} else {
return firstName;
}
}
let tomcat = buildName("Tom", "Cat");
let tom = buildName("Tom");

参数默认值:

在 ES6 中,我们允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数:

1
2
3
4
5
function buildName(firstName: string, lastName: string = "Cat") {
return firstName + " " + lastName;
}
let tomcat = buildName("Tom", "Cat");
let tom = buildName("Tom");

此时就不受「可选参数必须接在必需参数后面」的限制了:

1
2
3
4
5
function buildName(firstName: string = "Tom", lastName: string) {
return firstName + " " + lastName;
}
let tomcat = buildName("Tom", "Cat");
let cat = buildName(undefined, "Cat");

剩余参数:

ES6 中,可以使用 <font style="color:rgb(51, 51, 51);">...rest</font> 的方式获取函数中的剩余参数(rest 参数):

1
2
3
4
5
6
7
8
function push(array, ...items) {
items.forEach(function (item) {
array.push(item);
});
}

let a: any[] = [];
push(a, 1, 2, 3);

事实上,<font style="color:rgb(51, 51, 51);">items </font>是一个数组。所以我们可以用数组的类型来定义它:

1
2
3
4
5
6
7
8
function push(array: any[], ...items: any[]) {
items.forEach(function (item) {
array.push(item);
});
}

let a = [];
push(a, 1, 2, 3);

函数重载:

重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。

比如,我们需要实现一个函数 reverse,输入数字 123 的时候,输出反转的数字 321,输入字符串 ‘hello’ 的时候,输出反转的字符串 ‘olleh’

利用联合类型,我们可以这么实现:

1
2
3
4
5
6
7
function reverse(x: number | string): number | string | void {
if (typeof x === "number") {
return Number(x.toString().split("").reverse().join(""));
} else if (typeof x === "string") {
return x.split("").reverse().join("");
}
}

然而这样有一个缺点,就是不能够精确的表达,输入为数字的时候,输出也应该为数字,输入为字符串的时候,输出也应该为字符串。

这时,我们可以使用重载定义多个 reverse 的函数类型:

1
2
3
4
5
6
7
8
9
function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string | void {
if (typeof x === "number") {
return Number(x.toString().split("").reverse().join(""));
} else if (typeof x === "string") {
return x.split("").reverse().join("");
}
}

上例中,我们重复定义了多次函数 reverse,前几次都是函数定义,最后一次是函数实现。在编辑器的代码提示中,可以正确的看到前两个提示。

注意,TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。

类类型

下面看一个使用类的例子:

1
2
3
4
5
6
7
8
9
10
11
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}

let greeter = new Greeter("world");

继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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);
}
}

class Horse extends Animal {
constructor(name: string) {
super(name);
}
move(distanceInMeters = 45) {
console.log("Galloping...");
super.move(distanceInMeters);
}
}

let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");

sam.move();
tom.move(34);

公共,私有与受保护的修饰符:

  • public:默认的修饰符,允许在类的外部和派生类访问
  • private:不允许在声明它的类的外部访问
  • protected:允许在派生类中访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person {
protected name: string;
constructor(name: string) {
this.name = name;
}
}

class Employee extends Person {
private department: string;

constructor(name: string, department: string) {
super(name);
this.department = department;
}

public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}

let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // 错误

**属性只读:
**你可以使用 readonly 关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。

1
2
3
4
5
6
7
8
9
class Octopus {
readonly name: string;
readonly numberOfLegs: number = 8;
constructor(theName: string) {
this.name = theName;
}
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // 错误! name 是只读的.

静态属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Grid {
static origin = { x: 0, y: 0 };
calculateDistanceFromOrigin(point: { x: number; y: number }) {
let xDist = point.x - Grid.origin.x;
let yDist = point.y - Grid.origin.y;
return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
}
constructor(public scale: number) {}
}

let grid1 = new Grid(1.0); // 1x scale
let grid2 = new Grid(5.0); // 5x scale

console.log(grid1.calculateDistanceFromOrigin({ x: 10, y: 10 }));
console.log(grid2.calculateDistanceFromOrigin({ x: 10, y: 10 }));

抽象类:

抽象类的特性如下:

  • 抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化
  • 抽象类可以包含成员的实现细节
  • abstract 关键字是用于定义抽象类和在抽象类内部定义抽象方法
  • 抽象类中的抽象方法不包含具体实现并且必须在派生类中实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
abstract class Department {
constructor(public name: string) {}

printName(): void {
console.log("Department name: " + this.name);
}

abstract printMeeting(): void; // 必须在派生类中实现
}

class AccountingDepartment extends Department {
constructor() {
super("Accounting and Auditing"); // 在派生类的构造函数中必须调用 super()
}

printMeeting(): void {
console.log("The Accounting Department meets each Monday at 10am.");
}

generateReports(): void {
console.log("Generating accounting reports...");
}
}

let department: Department; // 允许创建一个对抽象类型的引用
department = new Department(); // 错误: 不能创建一个抽象类的实例
department = new AccountingDepartment(); // 允许对一个抽象子类进行实例化和赋值
department.printName();
department.printMeeting();
department.generateReports(); // 错误: 方法在声明的抽象类中不存在

**
**