112 | //
113 | //
119 | // `
123 | // }}
124 | // />
125 | // ),
126 |
127 | //
128 | gitalk: {
129 | clientID: '29aa4941759fc887ed4f',
130 | clientSecret: '33e355efdf3a1959624506a5d88311145208471b',
131 | repo: 'typescript-tutorial',
132 | owner: 'xcatliu',
133 | admin: ['xcatliu'],
134 | pagerDirection: 'first',
135 | },
136 | ga: {
137 | id: 'UA-45256157-14',
138 | },
139 | port: 8001,
140 | };
141 |
--------------------------------------------------------------------------------
/advanced/class-and-interfaces.md:
--------------------------------------------------------------------------------
1 | # 类与接口
2 |
3 | [之前学习过](../basics/type-of-object-interfaces.md),接口(Interfaces)可以用于对「对象的形状(Shape)」进行描述。
4 |
5 | 这一章主要介绍接口的另一个用途,对类的一部分行为进行抽象。
6 |
7 | ## 类实现接口
8 |
9 | 实现(implements)是面向对象中的一个重要概念。一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用 `implements` 关键字来实现。这个特性大大提高了面向对象的灵活性。
10 |
11 | 举例来说,门是一个类,防盗门是门的子类。如果防盗门有一个报警器的功能,我们可以简单的给防盗门添加一个报警方法。这时候如果有另一个类,车,也有报警器的功能,就可以考虑把报警器提取出来,作为一个接口,防盗门和车都去实现它:
12 |
13 | ```ts
14 | interface Alarm {
15 | alert(): void;
16 | }
17 |
18 | class Door {
19 | }
20 |
21 | class SecurityDoor extends Door implements Alarm {
22 | alert() {
23 | console.log('SecurityDoor alert');
24 | }
25 | }
26 |
27 | class Car implements Alarm {
28 | alert() {
29 | console.log('Car alert');
30 | }
31 | }
32 | ```
33 |
34 | 一个类可以实现多个接口:
35 |
36 | ```ts
37 | interface Alarm {
38 | alert(): void;
39 | }
40 |
41 | interface Light {
42 | lightOn(): void;
43 | lightOff(): void;
44 | }
45 |
46 | class Car implements Alarm, Light {
47 | alert() {
48 | console.log('Car alert');
49 | }
50 | lightOn() {
51 | console.log('Car light on');
52 | }
53 | lightOff() {
54 | console.log('Car light off');
55 | }
56 | }
57 | ```
58 |
59 | 上例中,`Car` 实现了 `Alarm` 和 `Light` 接口,既能报警,也能开关车灯。
60 |
61 | ## 接口继承接口
62 |
63 | 接口与接口之间可以是继承关系:
64 |
65 | ```ts
66 | interface Alarm {
67 | alert(): void;
68 | }
69 |
70 | interface LightableAlarm extends Alarm {
71 | lightOn(): void;
72 | lightOff(): void;
73 | }
74 | ```
75 |
76 | 这很好理解,`LightableAlarm` 继承了 `Alarm`,除了拥有 `alert` 方法之外,还拥有两个新方法 `lightOn` 和 `lightOff`。
77 |
78 | ## 接口继承类
79 |
80 | 常见的面向对象语言中,接口是不能继承类的,但是在 TypeScript 中却是可以的:
81 |
82 | ```ts
83 | class Point {
84 | x: number;
85 | y: number;
86 | constructor(x: number, y: number) {
87 | this.x = x;
88 | this.y = y;
89 | }
90 | }
91 |
92 | interface Point3d extends Point {
93 | z: number;
94 | }
95 |
96 | let point3d: Point3d = {x: 1, y: 2, z: 3};
97 | ```
98 |
99 | 为什么 TypeScript 会支持接口继承类呢?
100 |
101 | 实际上,当我们在声明 `class Point` 时,除了会创建一个名为 `Point` 的类之外,同时也创建了一个名为 `Point` 的类型(实例的类型)。
102 |
103 | 所以我们既可以将 `Point` 当做一个类来用(使用 `new Point` 创建它的实例):
104 |
105 | ```ts
106 | class Point {
107 | x: number;
108 | y: number;
109 | constructor(x: number, y: number) {
110 | this.x = x;
111 | this.y = y;
112 | }
113 | }
114 |
115 | const p = new Point(1, 2);
116 | ```
117 |
118 | 也可以将 `Point` 当做一个类型来用(使用 `: Point` 表示参数的类型):
119 |
120 | ```ts
121 | class Point {
122 | x: number;
123 | y: number;
124 | constructor(x: number, y: number) {
125 | this.x = x;
126 | this.y = y;
127 | }
128 | }
129 |
130 | function printPoint(p: Point) {
131 | console.log(p.x, p.y);
132 | }
133 |
134 | printPoint(new Point(1, 2));
135 | ```
136 |
137 | 这个例子实际上可以等价于:
138 |
139 | ```ts
140 | class Point {
141 | x: number;
142 | y: number;
143 | constructor(x: number, y: number) {
144 | this.x = x;
145 | this.y = y;
146 | }
147 | }
148 |
149 | interface PointInstanceType {
150 | x: number;
151 | y: number;
152 | }
153 |
154 | function printPoint(p: PointInstanceType) {
155 | console.log(p.x, p.y);
156 | }
157 |
158 | printPoint(new Point(1, 2));
159 | ```
160 |
161 | 上例中我们新声明的 `PointInstanceType` 类型,与声明 `class Point` 时创建的 `Point` 类型是等价的。
162 |
163 | 所以回到 `Point3d` 的例子中,我们就能很容易的理解为什么 TypeScript 会支持接口继承类了:
164 |
165 | ```ts
166 | class Point {
167 | x: number;
168 | y: number;
169 | constructor(x: number, y: number) {
170 | this.x = x;
171 | this.y = y;
172 | }
173 | }
174 |
175 | interface PointInstanceType {
176 | x: number;
177 | y: number;
178 | }
179 |
180 | // 等价于 interface Point3d extends PointInstanceType
181 | interface Point3d extends Point {
182 | z: number;
183 | }
184 |
185 | let point3d: Point3d = {x: 1, y: 2, z: 3};
186 | ```
187 |
188 | 当我们声明 `interface Point3d extends Point` 时,`Point3d` 继承的实际上是类 `Point` 的实例的类型。
189 |
190 | 换句话说,可以理解为定义了一个接口 `Point3d` 继承另一个接口 `PointInstanceType`。
191 |
192 | 所以「接口继承类」和「接口继承接口」没有什么本质的区别。
193 |
194 | 值得注意的是,`PointInstanceType` 相比于 `Point`,缺少了 `constructor` 方法,这是因为声明 `Point` 类时创建的 `Point` 类型是不包含构造函数的。另外,除了构造函数是不包含的,静态属性或静态方法也是不包含的(实例的类型当然不应该包括构造函数、静态属性或静态方法)。
195 |
196 | 换句话说,声明 `Point` 类时创建的 `Point` 类型只包含其中的实例属性和实例方法:
197 |
198 | ```ts
199 | class Point {
200 | /** 静态属性,坐标系原点 */
201 | static origin = new Point(0, 0);
202 | /** 静态方法,计算与原点距离 */
203 | static distanceToOrigin(p: Point) {
204 | return Math.sqrt(p.x * p.x + p.y * p.y);
205 | }
206 | /** 实例属性,x 轴的值 */
207 | x: number;
208 | /** 实例属性,y 轴的值 */
209 | y: number;
210 | /** 构造函数 */
211 | constructor(x: number, y: number) {
212 | this.x = x;
213 | this.y = y;
214 | }
215 | /** 实例方法,打印此点 */
216 | printPoint() {
217 | console.log(this.x, this.y);
218 | }
219 | }
220 |
221 | interface PointInstanceType {
222 | x: number;
223 | y: number;
224 | printPoint(): void;
225 | }
226 |
227 | let p1: Point;
228 | let p2: PointInstanceType;
229 | ```
230 |
231 | 上例中最后的类型 `Point` 和类型 `PointInstanceType` 是等价的。
232 |
233 | 同样的,在接口继承类的时候,也只会继承它的实例属性和实例方法。
234 |
235 | ## 参考
236 |
237 | - [Interfaces](http://www.typescriptlang.org/docs/handbook/interfaces.html)([中文版](https://zhongsp.gitbooks.io/typescript-handbook/content/doc/handbook/Interfaces.html))
238 |
--------------------------------------------------------------------------------
/advanced/generics.md:
--------------------------------------------------------------------------------
1 | # 泛型
2 |
3 | 泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
4 |
5 | ## 简单的例子
6 |
7 | 首先,我们来实现一个函数 `createArray`,它可以创建一个指定长度的数组,同时将每一项都填充一个默认值:
8 |
9 | ```ts
10 | function createArray(length: number, value: any): Array
{
11 | let result = [];
12 | for (let i = 0; i < length; i++) {
13 | result[i] = value;
14 | }
15 | return result;
16 | }
17 |
18 | createArray(3, 'x'); // ['x', 'x', 'x']
19 | ```
20 |
21 | 上例中,我们使用了[之前提到过的数组泛型](../basics/type-of-array.md#数组泛型)来定义返回值的类型。
22 |
23 | 这段代码编译不会报错,但是一个显而易见的缺陷是,它并没有准确的定义返回值的类型:
24 |
25 | `Array` 允许数组的每一项都为任意类型。但是我们预期的是,数组中每一项都应该是输入的 `value` 的类型。
26 |
27 | 这时候,泛型就派上用场了:
28 |
29 | ```ts
30 | function createArray(length: number, value: T): Array {
31 | let result: T[] = [];
32 | for (let i = 0; i < length; i++) {
33 | result[i] = value;
34 | }
35 | return result;
36 | }
37 |
38 | createArray(3, 'x'); // ['x', 'x', 'x']
39 | ```
40 |
41 | 上例中,我们在函数名后添加了 ``,其中 `T` 用来指代任意输入的类型,在后面的输入 `value: T` 和输出 `Array` 中即可使用了。
42 |
43 | 接着在调用的时候,可以指定它具体的类型为 `string`。当然,也可以不手动指定,而让类型推论自动推算出来:
44 |
45 | ```ts
46 | function createArray(length: number, value: T): Array {
47 | let result: T[] = [];
48 | for (let i = 0; i < length; i++) {
49 | result[i] = value;
50 | }
51 | return result;
52 | }
53 |
54 | createArray(3, 'x'); // ['x', 'x', 'x']
55 | ```
56 |
57 | ## 多个类型参数
58 |
59 | 定义泛型的时候,可以一次定义多个类型参数:
60 |
61 | ```ts
62 | function swap(tuple: [T, U]): [U, T] {
63 | return [tuple[1], tuple[0]];
64 | }
65 |
66 | swap([7, 'seven']); // ['seven', 7]
67 | ```
68 |
69 | 上例中,我们定义了一个 `swap` 函数,用来交换输入的元组。
70 |
71 | ## 泛型约束
72 |
73 | 在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法:
74 |
75 | ```ts
76 | function loggingIdentity(arg: T): T {
77 | console.log(arg.length);
78 | return arg;
79 | }
80 |
81 | // index.ts(2,19): error TS2339: Property 'length' does not exist on type 'T'.
82 | ```
83 |
84 | 上例中,泛型 `T` 不一定包含属性 `length`,所以编译的时候报错了。
85 |
86 | 这时,我们可以对泛型进行约束,只允许这个函数传入那些包含 `length` 属性的变量。这就是泛型约束:
87 |
88 | ```ts
89 | interface Lengthwise {
90 | length: number;
91 | }
92 |
93 | function loggingIdentity(arg: T): T {
94 | console.log(arg.length);
95 | return arg;
96 | }
97 | ```
98 |
99 | 上例中,我们使用了 `extends` 约束了泛型 `T` 必须符合接口 `Lengthwise` 的形状,也就是必须包含 `length` 属性。
100 |
101 | 此时如果调用 `loggingIdentity` 的时候,传入的 `arg` 不包含 `length`,那么在编译阶段就会报错了:
102 |
103 | ```ts
104 | interface Lengthwise {
105 | length: number;
106 | }
107 |
108 | function loggingIdentity(arg: T): T {
109 | console.log(arg.length);
110 | return arg;
111 | }
112 |
113 | loggingIdentity(7);
114 |
115 | // index.ts(10,17): error TS2345: Argument of type '7' is not assignable to parameter of type 'Lengthwise'.
116 | ```
117 |
118 | 多个类型参数之间也可以互相约束:
119 |
120 | ```ts
121 | function copyFields(target: T, source: U): T {
122 | for (let id in source) {
123 | target[id] = (source)[id];
124 | }
125 | return target;
126 | }
127 |
128 | let x = { a: 1, b: 2, c: 3, d: 4 };
129 |
130 | copyFields(x, { b: 10, d: 20 });
131 | ```
132 |
133 | 上例中,我们使用了两个类型参数,其中要求 `T` 继承 `U`,这样就保证了 `U` 上不会出现 `T` 中不存在的字段。
134 |
135 | ## 泛型接口
136 |
137 | [之前学习过](../basics/type-of-function.md#接口中函数的定义),可以使用接口的方式来定义一个函数需要符合的形状:
138 |
139 | ```ts
140 | interface SearchFunc {
141 | (source: string, subString: string): boolean;
142 | }
143 |
144 | let mySearch: SearchFunc;
145 | mySearch = function(source: string, subString: string) {
146 | return source.search(subString) !== -1;
147 | }
148 | ```
149 |
150 | 当然也可以使用含有泛型的接口来定义函数的形状:
151 |
152 | ```ts
153 | interface CreateArrayFunc {
154 | (length: number, value: T): Array;
155 | }
156 |
157 | let createArray: CreateArrayFunc;
158 | createArray = function(length: number, value: T): Array {
159 | let result: T[] = [];
160 | for (let i = 0; i < length; i++) {
161 | result[i] = value;
162 | }
163 | return result;
164 | }
165 |
166 | createArray(3, 'x'); // ['x', 'x', 'x']
167 | ```
168 |
169 | 进一步,我们可以把泛型参数提前到接口名上:
170 |
171 | ```ts
172 | interface CreateArrayFunc {
173 | (length: number, value: T): Array;
174 | }
175 |
176 | let createArray: CreateArrayFunc;
177 | createArray = function(length: number, value: T): Array {
178 | let result: T[] = [];
179 | for (let i = 0; i < length; i++) {
180 | result[i] = value;
181 | }
182 | return result;
183 | }
184 |
185 | createArray(3, 'x'); // ['x', 'x', 'x']
186 | ```
187 |
188 | 注意,此时在使用泛型接口的时候,需要定义泛型的类型。
189 |
190 | ## 泛型类
191 |
192 | 与泛型接口类似,泛型也可以用于类的类型定义中:
193 |
194 | ```ts
195 | class GenericNumber {
196 | zeroValue: T;
197 | add: (x: T, y: T) => T;
198 | }
199 |
200 | let myGenericNumber = new GenericNumber();
201 | myGenericNumber.zeroValue = 0;
202 | myGenericNumber.add = function(x, y) { return x + y; };
203 | ```
204 |
205 | ## 泛型参数的默认类型
206 |
207 | 在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用。
208 |
209 | ```ts
210 | function createArray(length: number, value: T): Array {
211 | let result: T[] = [];
212 | for (let i = 0; i < length; i++) {
213 | result[i] = value;
214 | }
215 | return result;
216 | }
217 | ```
218 |
219 | ## 参考
220 |
221 | - [Generics](http://www.typescriptlang.org/docs/handbook/generics.html)([中文版](https://zhongsp.gitbooks.io/typescript-handbook/content/doc/handbook/generics.html))
222 | - [Generic parameter defaults](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-3.html#generic-parameter-defaults)
223 |
--------------------------------------------------------------------------------
/basics/type-of-object-interfaces.md:
--------------------------------------------------------------------------------
1 | # 对象的类型——接口
2 |
3 | 在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型。
4 |
5 | ## 什么是接口
6 |
7 | 在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。
8 |
9 | TypeScript 中的接口是一个非常灵活的概念,除了可用于[对类的一部分行为进行抽象](../advanced/class-and-interfaces.md#类实现接口)以外,也常用于对「对象的形状(Shape)」进行描述。
10 |
11 | ## 简单的例子
12 |
13 | ```ts
14 | interface Person {
15 | name: string;
16 | age: number;
17 | }
18 |
19 | let tom: Person = {
20 | name: 'Tom',
21 | age: 25
22 | };
23 | ```
24 |
25 | 上面的例子中,我们定义了一个接口 `Person`,接着定义了一个变量 `tom`,它的类型是 `Person`。这样,我们就约束了 `tom` 的形状必须和接口 `Person` 一致。
26 |
27 | 接口一般首字母大写。[有的编程语言中会建议接口的名称加上 `I` 前缀](https://msdn.microsoft.com/en-us/library/8bc1fexb%28v=vs.71%29.aspx)。
28 |
29 | 定义的变量比接口少了一些属性是不允许的:
30 |
31 | ```ts
32 | interface Person {
33 | name: string;
34 | age: number;
35 | }
36 |
37 | let tom: Person = {
38 | name: 'Tom'
39 | };
40 |
41 | // index.ts(6,5): error TS2322: Type '{ name: string; }' is not assignable to type 'Person'.
42 | // Property 'age' is missing in type '{ name: string; }'.
43 | ```
44 |
45 | 多一些属性也是不允许的:
46 |
47 | ```ts
48 | interface Person {
49 | name: string;
50 | age: number;
51 | }
52 |
53 | let tom: Person = {
54 | name: 'Tom',
55 | age: 25,
56 | gender: 'male'
57 | };
58 |
59 | // index.ts(9,5): error TS2322: Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'.
60 | // Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.
61 | ```
62 |
63 | 可见,**赋值的时候,变量的形状必须和接口的形状保持一致**。
64 |
65 | ## 可选属性
66 |
67 | 有时我们希望不要完全匹配一个形状,那么可以用可选属性:
68 |
69 | ```ts
70 | interface Person {
71 | name: string;
72 | age?: number;
73 | }
74 |
75 | let tom: Person = {
76 | name: 'Tom'
77 | };
78 | ```
79 |
80 | ```ts
81 | interface Person {
82 | name: string;
83 | age?: number;
84 | }
85 |
86 | let tom: Person = {
87 | name: 'Tom',
88 | age: 25
89 | };
90 | ```
91 |
92 | 可选属性的含义是该属性可以不存在。
93 |
94 | 这时**仍然不允许添加未定义的属性**:
95 |
96 | ```ts
97 | interface Person {
98 | name: string;
99 | age?: number;
100 | }
101 |
102 | let tom: Person = {
103 | name: 'Tom',
104 | age: 25,
105 | gender: 'male'
106 | };
107 |
108 | // examples/playground/index.ts(9,5): error TS2322: Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'.
109 | // Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.
110 | ```
111 |
112 | ## 任意属性
113 |
114 | 有时候我们希望一个接口允许有任意的属性,可以使用如下方式:
115 |
116 | ```ts
117 | interface Person {
118 | name: string;
119 | age?: number;
120 | [propName: string]: any;
121 | }
122 |
123 | let tom: Person = {
124 | name: 'Tom',
125 | gender: 'male'
126 | };
127 | ```
128 |
129 | 使用 `[propName: string]` 定义了任意属性取 `string` 类型的值。
130 |
131 | 需要注意的是,**一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集**:
132 |
133 | ```ts
134 | interface Person {
135 | name: string;
136 | age?: number;
137 | [propName: string]: string;
138 | }
139 |
140 | let tom: Person = {
141 | name: 'Tom',
142 | age: 25,
143 | gender: 'male'
144 | };
145 |
146 | // index.ts(3,5): error TS2411: Property 'age' of type 'number' is not assignable to string index type 'string'.
147 | // index.ts(7,5): error TS2322: Type '{ [x: string]: string | number; name: string; age: number; gender: string; }' is not assignable to type 'Person'.
148 | // Index signatures are incompatible.
149 | // Type 'string | number' is not assignable to type 'string'.
150 | // Type 'number' is not assignable to type 'string'.
151 | ```
152 |
153 | 上例中,任意属性的值允许是 `string`,但是可选属性 `age` 的值却是 `number`,`number` 不是 `string` 的子属性,所以报错了。
154 |
155 | 另外,在报错信息中可以看出,此时 `{ name: 'Tom', age: 25, gender: 'male' }` 的类型被推断成了 `{ [x: string]: string | number; name: string; age: number; gender: string; }`,这是联合类型和接口的结合。
156 |
157 | 一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:
158 | ```ts
159 | interface Person {
160 | name: string;
161 | age?: number;
162 | [propName: string]: string | number;
163 | }
164 |
165 | let tom: Person = {
166 | name: 'Tom',
167 | age: 25,
168 | gender: 'male'
169 | };
170 | ```
171 |
172 | ## 只读属性
173 |
174 | 有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 `readonly` 定义只读属性:
175 |
176 | ```ts
177 | interface Person {
178 | readonly id: number;
179 | name: string;
180 | age?: number;
181 | [propName: string]: any;
182 | }
183 |
184 | let tom: Person = {
185 | id: 89757,
186 | name: 'Tom',
187 | gender: 'male'
188 | };
189 |
190 | tom.id = 9527;
191 |
192 | // index.ts(14,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.
193 | ```
194 |
195 | 上例中,使用 `readonly` 定义的属性 `id` 初始化后,又被赋值了,所以报错了。
196 |
197 | **注意,只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候**:
198 |
199 | ```ts
200 | interface Person {
201 | readonly id: number;
202 | name: string;
203 | age?: number;
204 | [propName: string]: any;
205 | }
206 |
207 | let tom: Person = {
208 | name: 'Tom',
209 | gender: 'male'
210 | };
211 |
212 | tom.id = 89757;
213 |
214 | // index.ts(8,5): error TS2322: Type '{ name: string; gender: string; }' is not assignable to type 'Person'.
215 | // Property 'id' is missing in type '{ name: string; gender: string; }'.
216 | // index.ts(13,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.
217 | ```
218 |
219 | 上例中,报错信息有两处,第一处是在对 `tom` 进行赋值的时候,没有给 `id` 赋值。
220 |
221 | 第二处是在给 `tom.id` 赋值的时候,由于它是只读属性,所以报错了。
222 |
223 | ## 参考
224 |
225 | - [Interfaces](http://www.typescriptlang.org/docs/handbook/interfaces.html)([中文版](https://zhongsp.gitbooks.io/typescript-handbook/content/doc/handbook/Interfaces.html))
226 |
--------------------------------------------------------------------------------
/advanced/enum.md:
--------------------------------------------------------------------------------
1 | # 枚举
2 |
3 | 枚举(Enum)类型用于取值被限定在一定范围内的场景,比如一周只能有七天,颜色限定为红绿蓝等。
4 |
5 | ## 简单的例子
6 |
7 | 枚举使用 `enum` 关键字来定义:
8 |
9 | ```ts
10 | enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
11 | ```
12 |
13 | 枚举成员会被赋值为从 `0` 开始递增的数字,同时也会对枚举值到枚举名进行反向映射:
14 |
15 | ```ts
16 | enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
17 |
18 | console.log(Days["Sun"] === 0); // true
19 | console.log(Days["Mon"] === 1); // true
20 | console.log(Days["Tue"] === 2); // true
21 | console.log(Days["Sat"] === 6); // true
22 |
23 | console.log(Days[0] === "Sun"); // true
24 | console.log(Days[1] === "Mon"); // true
25 | console.log(Days[2] === "Tue"); // true
26 | console.log(Days[6] === "Sat"); // true
27 | ```
28 |
29 | 事实上,上面的例子会被编译为:
30 |
31 | ```js
32 | var Days;
33 | (function (Days) {
34 | Days[Days["Sun"] = 0] = "Sun";
35 | Days[Days["Mon"] = 1] = "Mon";
36 | Days[Days["Tue"] = 2] = "Tue";
37 | Days[Days["Wed"] = 3] = "Wed";
38 | Days[Days["Thu"] = 4] = "Thu";
39 | Days[Days["Fri"] = 5] = "Fri";
40 | Days[Days["Sat"] = 6] = "Sat";
41 | })(Days || (Days = {}));
42 | ```
43 |
44 | ## 手动赋值
45 |
46 | 我们也可以给枚举项手动赋值:
47 |
48 | ```ts
49 | enum Days {Sun = 7, Mon = 1, Tue, Wed, Thu, Fri, Sat};
50 |
51 | console.log(Days["Sun"] === 7); // true
52 | console.log(Days["Mon"] === 1); // true
53 | console.log(Days["Tue"] === 2); // true
54 | console.log(Days["Sat"] === 6); // true
55 | ```
56 |
57 | 上面的例子中,未手动赋值的枚举项会接着上一个枚举项递增。
58 |
59 | 如果未手动赋值的枚举项与手动赋值的重复了,TypeScript 是不会察觉到这一点的:
60 |
61 | ```ts
62 | enum Days {Sun = 3, Mon = 1, Tue, Wed, Thu, Fri, Sat};
63 |
64 | console.log(Days["Sun"] === 3); // true
65 | console.log(Days["Wed"] === 3); // true
66 | console.log(Days[3] === "Sun"); // false
67 | console.log(Days[3] === "Wed"); // true
68 | ```
69 |
70 | 上面的例子中,递增到 `3` 的时候与前面的 `Sun` 的取值重复了,但是 TypeScript 并没有报错,导致 `Days[3]` 的值先是 `"Sun"`,而后又被 `"Wed"` 覆盖了。编译的结果是:
71 |
72 | ```js
73 | var Days;
74 | (function (Days) {
75 | Days[Days["Sun"] = 3] = "Sun";
76 | Days[Days["Mon"] = 1] = "Mon";
77 | Days[Days["Tue"] = 2] = "Tue";
78 | Days[Days["Wed"] = 3] = "Wed";
79 | Days[Days["Thu"] = 4] = "Thu";
80 | Days[Days["Fri"] = 5] = "Fri";
81 | Days[Days["Sat"] = 6] = "Sat";
82 | })(Days || (Days = {}));
83 | ```
84 |
85 | 所以使用的时候需要注意,最好不要出现这种覆盖的情况。
86 |
87 | 手动赋值的枚举项可以不是数字,此时需要使用类型断言来让 tsc 无视类型检查 (编译出的 js 仍然是可用的):
88 |
89 | ```ts
90 | enum Days {Sun = 7, Mon, Tue, Wed, Thu, Fri, Sat = "S"};
91 | ```
92 |
93 | ```js
94 | var Days;
95 | (function (Days) {
96 | Days[Days["Sun"] = 7] = "Sun";
97 | Days[Days["Mon"] = 8] = "Mon";
98 | Days[Days["Tue"] = 9] = "Tue";
99 | Days[Days["Wed"] = 10] = "Wed";
100 | Days[Days["Thu"] = 11] = "Thu";
101 | Days[Days["Fri"] = 12] = "Fri";
102 | Days[Days["Sat"] = "S"] = "Sat";
103 | })(Days || (Days = {}));
104 | ```
105 |
106 | 当然,手动赋值的枚举项也可以为小数或负数,此时后续未手动赋值的项的递增步长仍为 `1`:
107 |
108 | ```ts
109 | enum Days {Sun = 7, Mon = 1.5, Tue, Wed, Thu, Fri, Sat};
110 |
111 | console.log(Days["Sun"] === 7); // true
112 | console.log(Days["Mon"] === 1.5); // true
113 | console.log(Days["Tue"] === 2.5); // true
114 | console.log(Days["Sat"] === 6.5); // true
115 | ```
116 |
117 | ## 常数项和计算所得项
118 |
119 | 枚举项有两种类型:常数项(constant member)和计算所得项(computed member)。
120 |
121 | 前面我们所举的例子都是常数项,一个典型的计算所得项的例子:
122 |
123 | ```ts
124 | enum Color {Red, Green, Blue = "blue".length};
125 | ```
126 |
127 | 上面的例子中,`"blue".length` 就是一个计算所得项。
128 |
129 | 上面的例子不会报错,但是**如果紧接在计算所得项后面的是未手动赋值的项,那么它就会因为无法获得初始值而报错**:
130 |
131 | ```ts
132 | enum Color {Red = "red".length, Green, Blue};
133 |
134 | // index.ts(1,33): error TS1061: Enum member must have initializer.
135 | // index.ts(1,40): error TS1061: Enum member must have initializer.
136 | ```
137 |
138 | 下面是常数项和计算所得项的完整定义,部分引用自[中文手册 - 枚举]:
139 |
140 | 当满足以下条件时,枚举成员被当作是常数:
141 |
142 | - 不具有初始化函数并且之前的枚举成员是常数。在这种情况下,当前枚举成员的值为上一个枚举成员的值加 `1`。但第一个枚举元素是个例外。如果它没有初始化方法,那么它的初始值为 `0`。
143 | - 枚举成员使用常数枚举表达式初始化。常数枚举表达式是 TypeScript 表达式的子集,它可以在编译阶段求值。当一个表达式满足下面条件之一时,它就是一个常数枚举表达式:
144 | - 数字字面量
145 | - 引用之前定义的常数枚举成员(可以是在不同的枚举类型中定义的)如果这个成员是在同一个枚举类型中定义的,可以使用非限定名来引用
146 | - 带括号的常数枚举表达式
147 | - `+`, `-`, `~` 一元运算符应用于常数枚举表达式
148 | - `+`, `-`, `*`, `/`, `%`, `<<`, `>>`, `>>>`, `&`, `|`, `^` 二元运算符,常数枚举表达式做为其一个操作对象。若常数枚举表达式求值后为 NaN 或 Infinity,则会在编译阶段报错
149 |
150 | 所有其它情况的枚举成员被当作是需要计算得出的值。
151 |
152 | ## 常数枚举
153 |
154 | 常数枚举是使用 `const enum` 定义的枚举类型:
155 |
156 | ```ts
157 | const enum Directions {
158 | Up,
159 | Down,
160 | Left,
161 | Right
162 | }
163 |
164 | let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
165 | ```
166 |
167 | 常数枚举与普通枚举的区别是,它会在编译阶段被删除,并且不能包含计算成员。
168 |
169 | 上例的编译结果是:
170 |
171 | ```js
172 | var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];
173 | ```
174 |
175 | 假如包含了计算成员,则会在编译阶段报错:
176 |
177 | ```ts
178 | const enum Color {Red, Green, Blue = "blue".length};
179 |
180 | // index.ts(1,38): error TS2474: In 'const' enum declarations member initializer must be constant expression.
181 | ```
182 |
183 | ## 外部枚举
184 |
185 | 外部枚举(Ambient Enums)是使用 `declare enum` 定义的枚举类型:
186 |
187 | ```ts
188 | declare enum Directions {
189 | Up,
190 | Down,
191 | Left,
192 | Right
193 | }
194 |
195 | let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
196 | ```
197 |
198 | 之前提到过,`declare` 定义的类型只会用于编译时的检查,编译结果中会被删除。
199 |
200 | 上例的编译结果是:
201 |
202 | ```js
203 | var directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
204 | ```
205 |
206 | 外部枚举与声明语句一样,常出现在声明文件中。
207 |
208 | 同时使用 `declare` 和 `const` 也是可以的:
209 |
210 | ```ts
211 | declare const enum Directions {
212 | Up,
213 | Down,
214 | Left,
215 | Right
216 | }
217 |
218 | let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
219 | ```
220 |
221 | 编译结果:
222 |
223 | ```js
224 | var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];
225 | ```
226 |
227 | > TypeScript 的枚举类型的概念[来源于 C#][C# Enum]。
228 |
229 | ## 参考
230 |
231 | - [Enums](http://www.typescriptlang.org/docs/handbook/enums.html)([中文版][中文手册 - 枚举])
232 | - [C# Enum][]
233 |
234 | [中文手册 - 枚举]: https://zhongsp.gitbooks.io/typescript-handbook/content/doc/handbook/Enums.html
235 | [C# Enum]: https://msdn.microsoft.com/zh-cn/library/sbbt4032.aspx
236 |
--------------------------------------------------------------------------------
/basics/type-of-function.md:
--------------------------------------------------------------------------------
1 | # 函数的类型
2 |
3 | > [函数是 JavaScript 中的一等公民](https://llh911001.gitbooks.io/mostly-adequate-guide-chinese/content/ch2.html)
4 |
5 | ## 函数声明
6 |
7 | 在 JavaScript 中,有两种常见的定义函数的方式——函数声明(Function Declaration)和函数表达式(Function Expression):
8 |
9 | ```js
10 | // 函数声明(Function Declaration)
11 | function sum(x, y) {
12 | return x + y;
13 | }
14 |
15 | // 函数表达式(Function Expression)
16 | let mySum = function (x, y) {
17 | return x + y;
18 | };
19 | ```
20 |
21 | 一个函数有输入和输出,要在 TypeScript 中对其进行约束,需要把输入和输出都考虑到,其中函数声明的类型定义较简单:
22 |
23 | ```ts
24 | function sum(x: number, y: number): number {
25 | return x + y;
26 | }
27 | ```
28 |
29 | 注意,**输入多余的(或者少于要求的)参数,是不被允许的**:
30 |
31 | ```ts
32 | function sum(x: number, y: number): number {
33 | return x + y;
34 | }
35 | sum(1, 2, 3);
36 |
37 | // index.ts(4,1): error TS2346: Supplied parameters do not match any signature of call target.
38 | ```
39 |
40 | ```ts
41 | function sum(x: number, y: number): number {
42 | return x + y;
43 | }
44 | sum(1);
45 |
46 | // index.ts(4,1): error TS2346: Supplied parameters do not match any signature of call target.
47 | ```
48 |
49 | ## 函数表达式
50 |
51 | 如果要我们现在写一个函数表达式(Function Expression)的定义,可能会写成这样:
52 |
53 | ```ts
54 | let mySum = function (x: number, y: number): number {
55 | return x + y;
56 | };
57 | ```
58 |
59 | 这是可以通过编译的,不过事实上,上面的代码只对等号右侧的匿名函数进行了类型定义,而等号左边的 `mySum`,是通过赋值操作进行类型推论而推断出来的。如果需要我们手动给 `mySum` 添加类型,则应该是这样:
60 |
61 | ```ts
62 | let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
63 | return x + y;
64 | };
65 | ```
66 |
67 | 注意不要混淆了 TypeScript 中的 `=>` 和 ES6 中的 `=>`。
68 |
69 | 在 TypeScript 的类型定义中,`=>` 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。
70 |
71 | 在 ES6 中,`=>` 叫做箭头函数,应用十分广泛,可以参考 [ES6 中的箭头函数][]。
72 |
73 | ## 用接口定义函数的形状
74 |
75 | 我们也可以使用接口的方式来定义一个函数需要符合的形状:
76 |
77 | ```ts
78 | interface SearchFunc {
79 | (source: string, subString: string): boolean;
80 | }
81 |
82 | let mySearch: SearchFunc;
83 | mySearch = function(source: string, subString: string) {
84 | return source.search(subString) !== -1;
85 | }
86 | ```
87 |
88 | 采用函数表达式|接口定义函数的方式时,对等号左侧进行类型限制,可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。
89 |
90 | ## 可选参数
91 |
92 | 前面提到,输入多余的(或者少于要求的)参数,是不允许的。那么如何定义可选的参数呢?
93 |
94 | 与接口中的可选属性类似,我们用 `?` 表示可选的参数:
95 |
96 | ```ts
97 | function buildName(firstName: string, lastName?: string) {
98 | if (lastName) {
99 | return firstName + ' ' + lastName;
100 | } else {
101 | return firstName;
102 | }
103 | }
104 | let tomcat = buildName('Tom', 'Cat');
105 | let tom = buildName('Tom');
106 | ```
107 |
108 | 需要注意的是,可选参数必须接在必需参数后面。换句话说,**可选参数后面不允许再出现必需参数了**:
109 |
110 | ```ts
111 | function buildName(firstName?: string, lastName: string) {
112 | if (firstName) {
113 | return firstName + ' ' + lastName;
114 | } else {
115 | return lastName;
116 | }
117 | }
118 | let tomcat = buildName('Tom', 'Cat');
119 | let tom = buildName(undefined, 'Tom');
120 |
121 | // index.ts(1,40): error TS1016: A required parameter cannot follow an optional parameter.
122 | ```
123 |
124 | ## 参数默认值
125 |
126 | 在 ES6 中,我们允许给函数的参数添加默认值,**TypeScript 会将添加了默认值的参数识别为可选参数**:
127 |
128 | ```ts
129 | function buildName(firstName: string, lastName: string = 'Cat') {
130 | return firstName + ' ' + lastName;
131 | }
132 | let tomcat = buildName('Tom', 'Cat');
133 | let tom = buildName('Tom');
134 | ```
135 |
136 | 此时就不受「可选参数必须接在必需参数后面」的限制了:
137 |
138 | ```ts
139 | function buildName(firstName: string = 'Tom', lastName: string) {
140 | return firstName + ' ' + lastName;
141 | }
142 | let tomcat = buildName('Tom', 'Cat');
143 | let cat = buildName(undefined, 'Cat');
144 | ```
145 |
146 | > 关于默认参数,可以参考 [ES6 中函数参数的默认值][]。
147 |
148 | ## 剩余参数
149 |
150 | ES6 中,可以使用 `...rest` 的方式获取函数中的剩余参数(rest 参数):
151 |
152 | ```js
153 | function push(array, ...items) {
154 | items.forEach(function(item) {
155 | array.push(item);
156 | });
157 | }
158 |
159 | let a: any[] = [];
160 | push(a, 1, 2, 3);
161 | ```
162 |
163 | 事实上,`items` 是一个数组。所以我们可以用数组的类型来定义它:
164 |
165 | ```ts
166 | function push(array: any[], ...items: any[]) {
167 | items.forEach(function(item) {
168 | array.push(item);
169 | });
170 | }
171 |
172 | let a = [];
173 | push(a, 1, 2, 3);
174 | ```
175 |
176 | 注意,rest 参数只能是最后一个参数,关于 rest 参数,可以参考 [ES6 中的 rest 参数][]。
177 |
178 | ## 重载
179 |
180 | 重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。
181 |
182 | 比如,我们需要实现一个函数 `reverse`,输入数字 `123` 的时候,输出反转的数字 `321`,输入字符串 `'hello'` 的时候,输出反转的字符串 `'olleh'`。
183 |
184 | 利用联合类型,我们可以这么实现:
185 |
186 | ```ts
187 | function reverse(x: number | string): number | string | void {
188 | if (typeof x === 'number') {
189 | return Number(x.toString().split('').reverse().join(''));
190 | } else if (typeof x === 'string') {
191 | return x.split('').reverse().join('');
192 | }
193 | }
194 | ```
195 |
196 | **然而这样有一个缺点,就是不能够精确的表达,输入为数字的时候,输出也应该为数字,输入为字符串的时候,输出也应该为字符串。**
197 |
198 | 这时,我们可以使用重载定义多个 `reverse` 的函数类型:
199 |
200 | ```ts
201 | function reverse(x: number): number;
202 | function reverse(x: string): string;
203 | function reverse(x: number | string): number | string | void {
204 | if (typeof x === 'number') {
205 | return Number(x.toString().split('').reverse().join(''));
206 | } else if (typeof x === 'string') {
207 | return x.split('').reverse().join('');
208 | }
209 | }
210 | ```
211 |
212 | 上例中,我们重复定义了多次函数 `reverse`,前几次都是函数定义,最后一次是函数实现。在编辑器的代码提示中,可以正确的看到前两个提示。
213 |
214 | 注意,TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。
215 |
216 | ## 参考
217 |
218 | - [Functions](http://www.typescriptlang.org/docs/handbook/functions.html)([中文版](https://zhongsp.gitbooks.io/typescript-handbook/content/doc/handbook/Functions.html))
219 | - [Functions # Function Types](http://www.typescriptlang.org/docs/handbook/interfaces.html#function-types)([中文版](https://zhongsp.gitbooks.io/typescript-handbook/content/doc/handbook/Interfaces.html#函数类型))
220 | - [JS 函数式编程指南](https://llh911001.gitbooks.io/mostly-adequate-guide-chinese/content/)
221 | - [ES6 中的箭头函数]
222 | - [ES6 中函数参数的默认值]
223 | - [ES6 中的 rest 参数]
224 |
225 | [ES6 中的箭头函数]: http://es6.ruanyifeng.com/#docs/function#箭头函数
226 | [ES6 中函数参数的默认值]: http://es6.ruanyifeng.com/#docs/function#函数参数的默认值
227 | [ES6 中的 rest 参数]: http://es6.ruanyifeng.com/#docs/function#rest参数
228 |
--------------------------------------------------------------------------------
/introduction/what-is-typescript.md:
--------------------------------------------------------------------------------
1 | # 什么是 TypeScript
2 |
3 | > Typed JavaScript at Any Scale.
4 | > 添加了类型系统的 JavaScript,适用于任何规模的项目。
5 |
6 | 以上描述是官网[[1]](#link-1)对于 TypeScript 的定义。
7 |
8 | 它强调了 TypeScript 的两个最重要的特性——类型系统、适用于任何规模。
9 |
10 | ## TypeScript 的特性
11 |
12 | ### 类型系统
13 |
14 | 从 TypeScript 的名字就可以看出来,「类型」是其最核心的特性。
15 |
16 | 我们知道,JavaScript 是一门非常灵活的编程语言:
17 |
18 | - 它没有类型约束,一个变量可能初始化时是字符串,过一会儿又被赋值为数字。
19 | - 由于隐式类型转换的存在,有的变量的类型很难在运行前就确定。
20 | - 基于原型的面向对象编程,使得原型上的属性或方法可以在运行时被修改。
21 | - 函数是 JavaScript 中的一等公民[[2]](#link-2),可以赋值给变量,也可以当作参数或返回值。
22 |
23 | 这种灵活性就像一把双刃剑,一方面使得 JavaScript 蓬勃发展,无所不能,从 2013 年开始就一直蝉联最普遍使用的编程语言排行榜冠军[[3]](#link-3);另一方面也使得它的代码质量参差不齐,维护成本高,运行时错误多。
24 |
25 | 而 TypeScript 的类型系统,在很大程度上弥补了 JavaScript 的缺点。
26 |
27 | #### TypeScript 是静态类型
28 |
29 | 类型系统按照「类型检查的时机」来分类,可以分为动态类型和静态类型。
30 |
31 | 动态类型是指在运行时才会进行类型检查,这种语言的类型错误往往会导致运行时错误。JavaScript 是一门解释型语言[[4]](#link-4),没有编译阶段,所以它是动态类型,以下这段代码在运行时才会报错:
32 |
33 | ```js
34 | let foo = 1;
35 | foo.split(' ');
36 | // Uncaught TypeError: foo.split is not a function
37 | // 运行时会报错(foo.split 不是一个函数),造成线上 bug
38 | ```
39 |
40 | 静态类型是指编译阶段就能确定每个变量的类型,这种语言的类型错误往往会导致语法错误。TypeScript 在运行前需要先编译为 JavaScript,而在编译阶段就会进行类型检查,所以 **TypeScript 是静态类型**,这段 TypeScript 代码在编译阶段就会报错了:
41 |
42 | ```ts
43 | let foo = 1;
44 | foo.split(' ');
45 | // Property 'split' does not exist on type 'number'.
46 | // 编译时会报错(数字没有 split 方法),无法通过编译
47 | ```
48 |
49 | 你可能会奇怪,这段 TypeScript 代码看上去和 JavaScript 没有什么区别呀。
50 |
51 | 没错!大部分 JavaScript 代码都只需要经过少量的修改(或者完全不用修改)就变成 TypeScript 代码,这得益于 TypeScript 强大的[类型推论][],即使不去手动声明变量 `foo` 的类型,也能在变量初始化时自动推论出它是一个 `number` 类型。
52 |
53 | 完整的 TypeScript 代码是这样的:
54 |
55 | ```ts
56 | let foo: number = 1;
57 | foo.split(' ');
58 | // Property 'split' does not exist on type 'number'.
59 | // 编译时会报错(数字没有 split 方法),无法通过编译
60 | ```
61 |
62 | #### TypeScript 是弱类型
63 |
64 | 类型系统按照「是否允许隐式类型转换」来分类,可以分为强类型和弱类型。
65 |
66 | 以下这段代码不管是在 JavaScript 中还是在 TypeScript 中都是可以正常运行的,运行时数字 `1` 会被隐式类型转换为字符串 `'1'`,加号 `+` 被识别为字符串拼接,所以打印出结果是字符串 `'11'`。
67 |
68 | ```js
69 | console.log(1 + '1');
70 | // 打印出字符串 '11'
71 | ```
72 |
73 | TypeScript 是完全兼容 JavaScript 的,它不会修改 JavaScript 运行时的特性,所以**它们都是弱类型**。
74 |
75 | 作为对比,Python 是强类型,以下代码会在运行时报错:
76 |
77 | ```py
78 | print(1 + '1')
79 | # TypeError: unsupported operand type(s) for +: 'int' and 'str'
80 | ```
81 |
82 | 若要修复该错误,需要进行强制类型转换:
83 |
84 | ```py
85 | print(str(1) + '1')
86 | # 打印出字符串 '11'
87 | ```
88 |
89 | > 强/弱是相对的,Python 在处理整型和浮点型相加时,会将整型隐式转换为浮点型,但是这并不影响 Python 是强类型的结论,因为大部分情况下 Python 并不会进行隐式类型转换。相比而言,JavaScript 和 TypeScript 中不管加号两侧是什么类型,都可以通过隐式类型转换计算出一个结果——而不是报错——所以 JavaScript 和 TypeScript 都是弱类型。
90 |
91 | > 虽然 TypeScript 不限制加号两侧的类型,但是我们可以借助 TypeScript 提供的类型系统,以及 ESLint 提供的代码检查功能,来限制加号两侧必须同为数字或同为字符串[[5]](#link-5)。这在一定程度上使得 TypeScript 向「强类型」更近一步了——当然,这种限制是可选的。
92 |
93 | 这样的类型系统体现了 TypeScript 的核心设计理念[[6]](#link-6):在完整保留 JavaScript 运行时行为的基础上,通过引入静态类型系统来提高代码的可维护性,减少可能出现的 bug。
94 |
95 | ### 适用于任何规模
96 |
97 | TypeScript 非常适用于大型项目——这是显而易见的,类型系统可以为大型项目带来更高的可维护性,以及更少的 bug。
98 |
99 | 在中小型项目中推行 TypeScript 的最大障碍就是认为使用 TypeScript 需要写额外的代码,降低开发效率。但事实上,由于有[类型推论][],大部分类型都不需要手动声明了。相反,TypeScript 增强了编辑器(IDE)的功能,包括代码补全、接口提示、跳转到定义、代码重构等,这在很大程度上提高了开发效率。而且 TypeScript 有近百个[编译选项][],如果你认为类型检查过于严格,那么可以通过修改编译选项来降低类型检查的标准。
100 |
101 | TypeScript 还可以和 JavaScript 共存。这意味着如果你有一个使用 JavaScript 开发的旧项目,又想使用 TypeScript 的特性,那么你不需要急着把整个项目都迁移到 TypeScript,你可以使用 TypeScript 编写新文件,然后在后续更迭中逐步迁移旧文件。如果一些 JavaScript 文件的迁移成本太高,TypeScript 也提供了一个方案,可以让你在不修改 JavaScript 文件的前提下,编写一个[类型声明文件][],实现旧项目的渐进式迁移。
102 |
103 | 事实上,就算你从来没学习过 TypeScript,你也可能已经在不知不觉中使用到了 TypeScript——在 VSCode 编辑器中编写 JavaScript 时,代码补全和接口提示等功能就是通过 TypeScript Language Service 实现的[[7]](#link-7):
104 |
105 | 
106 |
107 | 一些第三方库原生支持了 TypeScript,在使用时就能获得代码补全了,比如 Vue 3.0[[8]](#link-8):
108 |
109 | 
110 |
111 | 有一些第三方库原生不支持 TypeScript,但是可以通过安装社区维护的类型声明库[[9]](#link-9)(比如通过运行 `npm install --save-dev @types/react` 来安装 React 的类型声明库)来获得代码补全能力——不管是在 JavaScript 项目中还是在 TypeScript 中项目中都是支持的:
112 |
113 | 
114 |
115 | 由此可见,TypeScript 的发展已经深入到前端社区的方方面面了,任何规模的项目都或多或少得到了 TypeScript 的支持。
116 |
117 | ### 与标准同步发展
118 |
119 | TypeScript 的另一个重要的特性就是坚持与 ECMAScript 标准[[10]](#link-10)同步发展。
120 |
121 | ECMAScript 是 JavaScript 核心语法的标准,自 2015 年起,每年都会发布一个新版本,包含一些新的语法。
122 |
123 | 一个新的语法从提案到变成正式标准,需要经历以下几个阶段:
124 |
125 | - Stage 0:展示阶段,仅仅是提出了讨论、想法,尚未正式提案。
126 | - Stage 1:征求意见阶段,提供抽象的 API 描述,讨论可行性,关键算法等。
127 | - Stage 2:草案阶段,使用正式的规范语言精确描述其语法和语义。
128 | - Stage 3:候选人阶段,语法的设计工作已完成,需要浏览器、Node.js 等环境支持,搜集用户的反馈。
129 | - Stage 4:定案阶段,已准备好将其添加到正式的 ECMAScript 标准中。
130 |
131 | 一个语法进入到 Stage 3 阶段后,TypeScript 就会实现它。一方面,让我们可以尽早的使用到最新的语法,帮助它进入到下一个阶段;另一方面,处于 Stage 3 阶段的语法已经比较稳定了,基本不会有语法的变更,这使得我们能够放心的使用它。
132 |
133 | 除了实现 ECMAScript 标准之外,TypeScript 团队也推进了诸多语法提案,比如可选链操作符(`?.`)[[11]](#link-11)、空值合并操作符(`??`)[[12]](#link-12)、Throw 表达式[[13]](#link-13)、正则匹配索引[[14]](#link-14)等。
134 |
135 | ## 总结
136 |
137 | 什么是 TypeScript?
138 |
139 | - TypeScript 是添加了类型系统的 JavaScript,适用于任何规模的项目。
140 | - TypeScript 是一门静态类型、弱类型的语言。
141 | - TypeScript 是完全兼容 JavaScript 的,它不会修改 JavaScript 运行时的特性。
142 | - TypeScript 可以编译为 JavaScript,然后运行在浏览器、Node.js 等任何能运行 JavaScript 的环境中。
143 | - TypeScript 拥有很多编译选项,类型检查的严格程度由你决定。
144 | - TypeScript 可以和 JavaScript 共存,这意味着 JavaScript 项目能够渐进式的迁移到 TypeScript。
145 | - TypeScript 增强了编辑器(IDE)的功能,提供了代码补全、接口提示、跳转到定义、代码重构等能力。
146 | - TypeScript 拥有活跃的社区,大多数常用的第三方库都提供了类型声明。
147 | - TypeScript 与标准同步发展,符合最新的 ECMAScript 标准(stage 3)。
148 |
149 | ## 附:TypeScript 的发展历史
150 |
151 | - 2012-10:微软发布了 TypeScript 第一个版本(0.8),此前已经在微软内部开发了两年。
152 | - 2014-04:TypeScript 发布了 1.0 版本。
153 | - 2014-10:Angular 发布了 2.0 版本,它是一个基于 TypeScript 开发的前端框架。
154 | - 2015-01:ts-loader 发布,webpack 可以编译 TypeScript 文件了。
155 | - 2015-04:微软发布了 Visual Studio Code,它内置了对 TypeScript 语言的支持,它自身也是用 TypeScript 开发的。
156 | - 2016-05:`@types/react` 发布,TypeScript 可以开发 React 应用了。
157 | - 2016-05:`@types/node` 发布,TypeScript 可以开发 Node.js 应用了。
158 | - 2016-09:TypeScript 发布了 2.0 版本。
159 | - 2018-06:TypeScript 发布了 3.0 版本。
160 | - 2019-02:TypeScript 宣布由官方团队来维护 typescript-eslint,以支持在 TypeScript 文件中运行 ESLint 检查。
161 | - 2020-05:Deno 发布了 1.0 版本,它是一个 JavaScript 和 TypeScript 运行时。
162 | - 2020-08:TypeScript 发布了 4.0 版本。
163 | - 2020-09:Vue 发布了 3.0 版本,官方支持 TypeScript。
164 |
165 | ## 参考资料
166 |
167 | 1. [TypeScript 官网](https://www.typescriptlang.org/)
168 | 2. [第 2 章: 一等公民的函数](https://llh911001.gitbooks.io/mostly-adequate-guide-chinese/content/ch2.html) · 函数式编程指北
169 | 3. [StackOverflow 2020 开发者调查报告](https://insights.stackoverflow.com/survey/2020)
170 | 4. [斯坦福 JavaScript 第一课](https://web.stanford.edu/class/cs98si/slides/overview.html)
171 | 5. [TypeScript ESLint 规则 `restrict-plus-operands`](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/restrict-plus-operands.md)
172 | 6. [TypeScript 设计理念](https://github.com/microsoft/TypeScript/wiki/TypeScript-Design-Goals)
173 | 7. [Visual Studio Code 中集成了 TypeScript](https://code.visualstudio.com/docs/languages/typescript)
174 | 8. [Vue 3.0 支持 TypeScript](https://v3.vuejs.org/guide/typescript-support.html)
175 | 9. [Definitely Typed](https://github.com/DefinitelyTyped/DefinitelyTyped)——TypeScript 团队帮助维护的类型定义仓库
176 | 10. [ECMAScript 标准](https://tc39.es/process-document/)
177 | 11. [可选链操作符(`?.`)](https://github.com/tc39/proposal-optional-chaining)
178 | 12. [空值合并操作符(`??`)](https://github.com/tc39/proposal-nullish-coalescing)
179 | 13. [Throw 表达式](https://github.com/tc39/proposal-throw-expressions)
180 | 14. [正则匹配索引](https://github.com/tc39/proposal-regexp-match-indices)
181 |
--------------------------------------------------------------------------------
/advanced/decorator.md:
--------------------------------------------------------------------------------
1 | # 装饰器
2 |
3 | 写在前面:本章只介绍 TypeScript 5.0+ 的装饰器用法,对于 5.0 以下的版本,请参考 [TypeScript 官方文档](https://www.typescriptlang.org/docs/handbook/decorators.html)
4 |
5 | ## 什么是装饰器
6 |
7 | 首先,什么是装饰器呢?[维基百科](https://en.wikipedia.org/wiki/Decorator_pattern)是这么说的:
8 |
9 | > In [object-oriented programming](https://en.wikipedia.org/wiki/Object-oriented_programming), the **decorator pattern** is a [design pattern](https://en.wikipedia.org/wiki/Design_pattern_(computer_science)) that allows behavior to be added to an individual [object](https://en.wikipedia.org/wiki/Object_(computer_science)), dynamically, without affecting the behavior of other instances of the same [class](https://en.wikipedia.org/wiki/Class_(computer_science)).
10 |
11 | 本人的蹩足翻译:在 OOP (面向对象编程)中,装饰器模式是一种允许动态地往一个对象上添加自定义行为,而又不影响该对象所属的类的其他实例的一种设计模式。
12 |
13 | > 什么是 OOP 和类?[前面的章节](https://ts.xcatliu.com/advanced/class.html)做过介绍。
14 |
15 | 这句话未免过于拗口了,我们不妨换个角度去切入。
16 |
17 | ## 装饰器的使用场景
18 |
19 | 要知道,一切设计模式的诞生,都是为了解决某个问题。在 JavaScript 的世界中,装饰器通常出现于以下场景:
20 |
21 | 1. 提供一种易读且容易实现的方式,修改类或者类的方法,避免出现大量重复的代码。
22 |
23 | 下面以修改类的方法为例。
24 |
25 | 首先,假设我们有一个 `Animal` 类:
26 |
27 | ```ts
28 | class Animal {
29 | type: string
30 | constructor(type: string) {
31 | this.type = type
32 | }
33 |
34 | greet() {
35 | console.log(`Hello, I'm a(n) ${this.type}!`)
36 | }
37 | }
38 |
39 | const xcat = new Animal('cat')
40 | xcat.greet() // Hello, I'm a(n) cat!
41 | ```
42 |
43 | 该类有一个 greet 方法,和调用方打招呼。
44 |
45 | 假如说,我还希望根据不同的 `type`,往 console 打印不同动物的叫声呢?
46 |
47 | 聪明的你或许想到了,这不就是**类的继承**吗!在子类的 `greet()` 方法中,实现不同的逻辑,再调用 `super.greet()` 即可。
48 |
49 | ```ts
50 | class Xcat extends Animal {
51 | constructor() {
52 | super('cat')
53 | }
54 |
55 | greet() {
56 | console.log('meow~ meow~')
57 | super.greet()
58 | }
59 | }
60 |
61 | const xcat = new Xcat()
62 | xcat.greet() // meow~ meow~
63 | // Hello, I'm a(n) cat!
64 | ```
65 |
66 | 用装饰器实现,也不妨为一种思路,比如在 `Animal` 类中,为 `greet()` 方法添加「打印不同动物叫声的」行为:
67 |
68 | ```ts
69 | class Animal {
70 | type: string
71 | constructor(type: string) {
72 | this.type = type
73 | }
74 |
75 | @yelling
76 | greet() {
77 | console.log(`Hello, I'm a(n) ${this.type}!`)
78 | }
79 | }
80 |
81 | const typeToYellingMap = {
82 | cat: 'meow~ meow~'
83 | }
84 |
85 | function yelling(originalMethod: any, context: ClassMethodDecoratorContext) {
86 | return function(...args: any[]) {
87 | console.log(typeToYellingMap[this.type])
88 | originalMethod.call(this, ...args)
89 | }
90 | }
91 |
92 | const xcat = new Animal('cat')
93 | xcat.greet() // meow~ meow~
94 | // Hello, I'm a(n) cat!
95 | ```
96 |
97 | 在 `Animal.greet()` 方法上出现的 `@yelling` ,就是 TypeScript 中装饰器的写法,即 @ + 函数名的组合。
98 |
99 | 上述示例对装饰器的应用属于**方法装饰器**,此类装饰器本身接收两个参数,一是被装饰的方法,二是方法装饰器的上下文。方法装饰器应返回一个函数,此函数在运行时真正被执行。在上述例子中,我们在装饰器返回的函数中做了两件事情:
100 |
101 | 1. 打印相应类别的动物的叫声。
102 | 2. 调用 `originalMethod.call(this, …args)` ,确保原方法(即装饰器所装饰的方法)能够正确地被执行。
103 | 2. 结合「**依赖注入**」这一设计模式,优化模块与 class 的依赖关系。
104 |
105 | 什么是依赖注入呢?引用同事 [zio](https://github.com/ziofat) 的原话:
106 |
107 | > **依赖注入其实是将一个模块所依赖的部分作为参数传入,而不是由模块自己去构造。**
108 |
109 | 可见,依赖注入解决了实际工程项目中,类、模块间依赖关系层级复杂的问题,将构造单例的行为交由实现依赖注入的框架去处理。
110 |
111 | 举个例子:
112 |
113 | ```ts
114 | @injectable
115 | class Dog implements IAnimal {
116 | sayHi() {
117 | console.log('woof woof woof')
118 | }
119 | }
120 |
121 | @injectable
122 | class Cat implements IAnimal {
123 | sayHi() {
124 | console.log('meow meow meow')
125 | }
126 | }
127 |
128 | class AnimalService {
129 | constructor(
130 | @inject dog: Dog
131 | @inject cat: Cat
132 | ) {
133 | this._dog = dog
134 | this._cat = cat
135 | }
136 |
137 | sayHiByDog() {
138 | this._dog.sayHi()
139 | }
140 |
141 | sayHiByCat() {
142 | this._cat.sayHi()
143 | }
144 | }
145 | ```
146 |
147 | 在上述代码中,`@injectable` 将一个类标记为「可被注入的」,在面向业务的类(即 `AnimalService`)中,使用 `@inject` 注入此类的单例,实现了「依赖倒置」。注意到这里的 `implements IAnimal` 用法,也是实战中依赖注入运用的精妙之处 —— 关心接口,而非具体实现。
148 |
149 | 3. 实现「AOP」,即 Aspect-oriented programming,面向切面编程。
150 |
151 | 所谓的「切面」,可以理解成,在复杂的各个业务维度中,只关注一个维度的事务。
152 |
153 | 例如,使用装饰器,实现对类的某个方法的执行时间记录:
154 |
155 | ```ts
156 | class MyService {
157 | @recordExecution
158 | myFn() {
159 | // do something...
160 | }
161 | }
162 |
163 | function recordExecution(originalMethod: any, context: ClassMethodDecoratorContext) {
164 | return function(...args: any[]) {
165 | console.time('mark execution')
166 | originalMethod.call(this, ...args)
167 | console.timeEnd('mark execution')
168 | }
169 | }
170 | ```
171 |
172 |
173 | ## 装饰器的类别
174 |
175 | 通过以上例子,相信读者已经对装饰器有一定了解,且认识到了装饰器在一些场景的强大之处。在此引用[阮一峰 es6 教程](https://es6.ruanyifeng.com/#docs/decorator#%E7%AE%80%E4%BB%8B%EF%BC%88%E6%96%B0%E8%AF%AD%E6%B3%95%EF%BC%89)稍做总结:
176 |
177 | > 装饰器是一种函数,写成`@ + 函数名`,可以用来装饰四种类型的值。
178 | >
179 | > - 类
180 | > - 类的属性
181 | > - 类的方法
182 | > - 属性存取器(accessor, getter, setter)
183 |
184 | > 装饰器的执行步骤如下。
185 | >
186 | > 1. 计算各个装饰器的值,按照从左到右,从上到下的顺序。
187 | > 2. 调用方法装饰器。
188 | > 3. 调用类装饰器。
189 |
190 | 不管是哪种类型的装饰器,它们的函数签名都可以认为是一致的,即均接收 `value`, `context` 两个参数,前者指被装饰的对象,后者指一个存储了上下文信息的对象。
191 |
192 | ## context 与 metadata 二三讲
193 |
194 | 四种装饰器的 context,均包含以下信息:
195 |
196 | - kind
197 |
198 | 描述被装饰的 value 的类型,可取 `class`, `method`, `field`, `getter`, `setter`, `accessor` 这些值。
199 |
200 | - name
201 |
202 | 描述被装饰的 value 的名字。
203 |
204 | - addInitializer
205 |
206 | 一个方法,接收一个回调函数,使得开发者可以侵入 value 的初始化过程作修改。
207 |
208 | 对 `class` 来说,这个回调函数会在类定义最终确认后调用,即相当于在初始化过程的最后一步。
209 |
210 | 对其他的 value 来说,如果是被 `static` 所修饰的,则会在类定义期间被调用,且早于其他静态属性的赋值过程;否则,会在类初始化期间被调用,且早于 value 自身的初始化。
211 |
212 | 以下是 `@bound` 类方法装饰器的例子,该装饰器自动为方法绑定 `this`:
213 |
214 | ```ts
215 | const bound = (value, context: ClassMemberDecoratorContext) {
216 | if (context.private) throw new TypeError("Not supported on private methods.");
217 | context.addInitializer(function () {
218 | this[context.name] = this[context.name].bind(this);
219 | });
220 | }
221 | ```
222 |
223 | - metadata
224 |
225 | 和装饰器类似,[metadata](https://github.com/tc39/proposal-decorator-metadata) 也是处于 stage 3 阶段的一个提案。装饰器只能访问到类原型链、类实例的相关数据,而 metadata 给了开发者更大的自由,让程序于运行时访问到编译时决定的元数据。
226 |
227 | 举个例子:
228 |
229 | ```ts
230 | function meta(key, value) {
231 | return (_, context) => {
232 | context.metadata[key] = value;
233 | };
234 | }
235 |
236 | @meta('a', 'x')
237 | class C {
238 | @meta('b', 'y')
239 | m() {}
240 | }
241 |
242 | C[Symbol.metadata].a; // 'x'
243 | C[Symbol.metadata].b; // 'y'
244 | ```
245 |
246 | 在上述程序中,我们通过访问类的 `Symbol.metadata` ,读取到了 meta 装饰器所写入的元数据。对元数据的访问,有且仅有这一种形式。
247 |
248 | 注意一点,metadata 是作用在类上的,即使它的位置在类方法上。想实现细粒度的元数据存储,可以考虑手动维护若干 `WeakMap`。
249 |
250 |
251 | 除了类装饰器以外,其他3种装饰器的 context 还拥有以下 3 个字段:
252 |
253 | - static
254 |
255 | 布尔值,描述 value 是否为 static 所修饰。
256 |
257 | - private
258 |
259 | 布尔值,描述 value 是否为 private 所修饰。
260 |
261 | - access
262 |
263 | 一个对象,可在运行时访问 value 相关数据。
264 |
265 | 以类方法装饰器为例,用 `access.get` 可在运行时读取方法值,`access.has` 可在运行时查询对象上是否有某方法,举个例子:
266 |
267 | ```ts
268 | const typeToYellingMap = {
269 | cat: 'meow~ meow~',
270 | }
271 |
272 | let yellingMethodContext: ClassMethodDecoratorContext
273 |
274 | class Animal {
275 | type: string
276 | constructor(type: string) {
277 | this.type = type
278 | }
279 |
280 | @yelling
281 | greet() {
282 | console.log(`Hello, I'm a(n) ${this.type}!`)
283 | }
284 |
285 | accessor y = 1
286 | }
287 |
288 | function yelling(originalMethod: any, context: ClassMethodDecoratorContext) {
289 | yellingMethodContext = context
290 | return function (this: any, ...args: any[]) {
291 | console.log(typeToYellingMap[this.type as keyof typeof typeToYellingMap])
292 | originalMethod.call(this, ...args)
293 | }
294 | }
295 |
296 | const xcat = new Animal('cat')
297 | xcat.greet() // meow~ meow~
298 | // Hello, I'm a(n) cat!
299 | yellingMethodContext.access.get(xcat).call(xcat) // meow~ meow~
300 | // Hello, I'm a(n) cat!
301 | console.log(yellingMethodContext.access.has(xcat)) // true
302 | ```
303 |
304 | `getter` 类别的装饰器,其 `context.access` 同样拥有 `has`, `get` 两个方法。
305 |
306 | 对于 `setter` 类别的装饰器,则是 `has` 与 `set` 方法。
307 |
308 | `filed` 与 `accessor` 类别的装饰器,拥有 `has`, `get`, `set` 全部三个方法。
309 |
--------------------------------------------------------------------------------
/advanced/class.md:
--------------------------------------------------------------------------------
1 | # 类
2 |
3 | 传统方法中,JavaScript 通过构造函数实现类的概念,通过原型链实现继承。而在 ES6 中,我们终于迎来了 `class`。
4 |
5 | TypeScript 除了实现了所有 ES6 中的类的功能以外,还添加了一些新的用法。
6 |
7 | 这一节主要介绍类的用法,下一节再介绍如何定义类的类型。
8 |
9 | ## 类的概念
10 |
11 | 虽然 JavaScript 中有类的概念,但是可能大多数 JavaScript 程序员并不是非常熟悉类,这里对类相关的概念做一个简单的介绍。
12 |
13 | - 类(Class):定义了一件事物的抽象特点,包含它的属性和方法
14 | - 对象(Object):类的实例,通过 `new` 生成
15 | - 面向对象(OOP)的三大特性:封装、继承、多态
16 | - 封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据
17 | - 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
18 | - 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如 `Cat` 和 `Dog` 都继承自 `Animal`,但是分别实现了自己的 `eat` 方法。此时针对某一个实例,我们无需了解它是 `Cat` 还是 `Dog`,就可以直接调用 `eat` 方法,程序会自动判断出来应该如何执行 `eat`
19 | - 存取器(getter & setter):用以改变属性的读取和赋值行为
20 | - 修饰符(Modifiers):修饰符是一些关键字,用于限定成员或类型的性质。比如 `public` 表示公有属性或方法
21 | - 抽象类(Abstract Class):抽象类是供其他类继承的基类,抽象类不允许被实例化。抽象类中的抽象方法必须在子类中被实现
22 | - 接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(implements)。一个类只能继承自另一个类,但是可以实现多个接口
23 |
24 | ## ES6 中类的用法
25 |
26 | 下面我们先回顾一下 ES6 中类的用法,更详细的介绍可以参考 [ECMAScript 6 入门 - Class]。
27 |
28 | ### 属性和方法
29 |
30 | 使用 `class` 定义类,使用 `constructor` 定义构造函数。
31 |
32 | 通过 `new` 生成新实例的时候,会自动调用构造函数。
33 |
34 | ```js
35 | class Animal {
36 | name;
37 | constructor(name) {
38 | this.name = name;
39 | }
40 | sayHi() {
41 | return `My name is ${this.name}`;
42 | }
43 | }
44 |
45 | let a = new Animal('Jack');
46 | console.log(a.sayHi()); // My name is Jack
47 | ```
48 |
49 | ### 类的继承
50 |
51 | 使用 `extends` 关键字实现继承,子类中使用 `super` 关键字来调用父类的构造函数和方法。
52 |
53 | ```js
54 | class Cat extends Animal {
55 | constructor(name) {
56 | super(name); // 调用父类的 constructor(name)
57 | console.log(this.name);
58 | }
59 | sayHi() {
60 | return 'Meow, ' + super.sayHi(); // 调用父类的 sayHi()
61 | }
62 | }
63 |
64 | let c = new Cat('Tom'); // Tom
65 | console.log(c.sayHi()); // Meow, My name is Tom
66 | ```
67 |
68 | ### 存取器
69 |
70 | 使用 getter 和 setter 可以改变属性的赋值和读取行为:
71 |
72 | ```js
73 | class Animal {
74 | constructor(name) {
75 | this.name = name;
76 | }
77 | get name() {
78 | return 'Jack';
79 | }
80 | set name(value) {
81 | console.log('setter: ' + value);
82 | }
83 | }
84 |
85 | let a = new Animal('Kitty'); // setter: Kitty
86 | a.name = 'Tom'; // setter: Tom
87 | console.log(a.name); // Jack
88 | ```
89 |
90 | ### 静态方法
91 |
92 | 使用 `static` 修饰符修饰的方法称为静态方法,它们不需要实例化,而是直接通过类来调用:
93 |
94 | ```js
95 | class Animal {
96 | static isAnimal(a) {
97 | return a instanceof Animal;
98 | }
99 | }
100 |
101 | let a = new Animal('Jack');
102 | Animal.isAnimal(a); // true
103 | a.isAnimal(a); // TypeError: a.isAnimal is not a function
104 | ```
105 |
106 | ## ES7 中类的用法
107 |
108 | ES7 中有一些关于类的提案,TypeScript 也实现了它们,这里做一个简单的介绍。
109 |
110 | ### 实例属性
111 |
112 | ES6 中实例的属性只能通过构造函数中的 `this.xxx` 来定义,ES7 提案中可以直接在类里面定义:
113 |
114 | ```js
115 | class Animal {
116 | name = 'Jack';
117 |
118 | constructor() {
119 | // ...
120 | }
121 | }
122 |
123 | let a = new Animal();
124 | console.log(a.name); // Jack
125 | ```
126 |
127 | ### 静态属性
128 |
129 | ES7 提案中,可以使用 `static` 定义一个静态属性:
130 |
131 | ```js
132 | class Animal {
133 | static num = 42;
134 |
135 | constructor() {
136 | // ...
137 | }
138 | }
139 |
140 | console.log(Animal.num); // 42
141 | ```
142 |
143 | ## TypeScript 中类的用法
144 |
145 | ### public private 和 protected
146 |
147 | TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 `public`、`private` 和 `protected`。
148 |
149 | - `public` 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 `public` 的
150 | - `private` 修饰的属性或方法是私有的,不能在声明它的类的外部访问
151 | - `protected` 修饰的属性或方法是受保护的,它和 `private` 类似,区别是它在子类中也是允许被访问的
152 |
153 | 下面举一些例子:
154 |
155 | ```ts
156 | class Animal {
157 | public name;
158 | public constructor(name) {
159 | this.name = name;
160 | }
161 | }
162 |
163 | let a = new Animal('Jack');
164 | console.log(a.name); // Jack
165 | a.name = 'Tom';
166 | console.log(a.name); // Tom
167 | ```
168 |
169 | 上面的例子中,`name` 被设置为了 `public`,所以直接访问实例的 `name` 属性是允许的。
170 |
171 | 很多时候,我们希望有的属性是无法直接存取的,这时候就可以用 `private` 了:
172 |
173 | ```ts
174 | class Animal {
175 | private name;
176 | public constructor(name) {
177 | this.name = name;
178 | }
179 | }
180 |
181 | let a = new Animal('Jack');
182 | console.log(a.name);
183 | a.name = 'Tom';
184 |
185 | // index.ts(9,13): error TS2341: Property 'name' is private and only accessible within class 'Animal'.
186 | // index.ts(10,1): error TS2341: Property 'name' is private and only accessible within class 'Animal'.
187 | ```
188 |
189 | 需要注意的是,TypeScript 编译之后的代码中,并没有限制 `private` 属性在外部的可访问性。
190 |
191 | 上面的例子编译后的代码是:
192 |
193 | ```js
194 | var Animal = (function () {
195 | function Animal(name) {
196 | this.name = name;
197 | }
198 | return Animal;
199 | })();
200 | var a = new Animal('Jack');
201 | console.log(a.name);
202 | a.name = 'Tom';
203 | ```
204 |
205 | 使用 `private` 修饰的属性或方法,在子类中也是不允许访问的:
206 |
207 | ```ts
208 | class Animal {
209 | private name;
210 | public constructor(name) {
211 | this.name = name;
212 | }
213 | }
214 |
215 | class Cat extends Animal {
216 | constructor(name) {
217 | super(name);
218 | console.log(this.name);
219 | }
220 | }
221 |
222 | // index.ts(11,17): error TS2341: Property 'name' is private and only accessible within class 'Animal'.
223 | ```
224 |
225 | 而如果是用 `protected` 修饰,则允许在子类中访问:
226 |
227 | ```ts
228 | class Animal {
229 | protected name;
230 | public constructor(name) {
231 | this.name = name;
232 | }
233 | }
234 |
235 | class Cat extends Animal {
236 | constructor(name) {
237 | super(name);
238 | console.log(this.name);
239 | }
240 | }
241 | ```
242 |
243 | 当构造函数修饰为 `private` 时,该类不允许被继承或者实例化:
244 |
245 | ```ts
246 | class Animal {
247 | public name;
248 | private constructor(name) {
249 | this.name = name;
250 | }
251 | }
252 | class Cat extends Animal {
253 | constructor(name) {
254 | super(name);
255 | }
256 | }
257 |
258 | let a = new Animal('Jack');
259 |
260 | // index.ts(7,19): TS2675: Cannot extend a class 'Animal'. Class constructor is marked as private.
261 | // index.ts(13,9): TS2673: Constructor of class 'Animal' is private and only accessible within the class declaration.
262 | ```
263 |
264 | 当构造函数修饰为 `protected` 时,该类只允许被继承:
265 |
266 | ```ts
267 | class Animal {
268 | public name;
269 | protected constructor(name) {
270 | this.name = name;
271 | }
272 | }
273 | class Cat extends Animal {
274 | constructor(name) {
275 | super(name);
276 | }
277 | }
278 |
279 | let a = new Animal('Jack');
280 |
281 | // index.ts(13,9): TS2674: Constructor of class 'Animal' is protected and only accessible within the class declaration.
282 | ```
283 |
284 | ### 参数属性
285 |
286 | 修饰符和`readonly`还可以使用在构造函数参数中,等同于类中定义该属性同时给该属性赋值,使代码更简洁。
287 |
288 | ```ts
289 | class Animal {
290 | // public name: string;
291 | public constructor(public name) {
292 | // this.name = name;
293 | }
294 | }
295 | ```
296 |
297 | ### readonly
298 |
299 | 只读属性关键字,只允许出现在属性声明或索引签名或构造函数中。
300 |
301 | ```ts
302 | class Animal {
303 | readonly name;
304 | public constructor(name) {
305 | this.name = name;
306 | }
307 | }
308 |
309 | let a = new Animal('Jack');
310 | console.log(a.name); // Jack
311 | a.name = 'Tom';
312 |
313 | // index.ts(10,3): TS2540: Cannot assign to 'name' because it is a read-only property.
314 | ```
315 |
316 | 注意如果 `readonly` 和其他访问修饰符同时存在的话,需要写在其后面。
317 |
318 | ```ts
319 | class Animal {
320 | // public readonly name;
321 | public constructor(public readonly name) {
322 | // this.name = name;
323 | }
324 | }
325 | ```
326 |
327 | ### 抽象类
328 |
329 | `abstract` 用于定义抽象类和其中的抽象方法。
330 |
331 | 什么是抽象类?
332 |
333 | 首先,抽象类是不允许被实例化的:
334 |
335 | ```ts
336 | abstract class Animal {
337 | public name;
338 | public constructor(name) {
339 | this.name = name;
340 | }
341 | public abstract sayHi();
342 | }
343 |
344 | let a = new Animal('Jack');
345 |
346 | // index.ts(9,11): error TS2511: Cannot create an instance of the abstract class 'Animal'.
347 | ```
348 |
349 | 上面的例子中,我们定义了一个抽象类 `Animal`,并且定义了一个抽象方法 `sayHi`。在实例化抽象类的时候报错了。
350 |
351 | 其次,抽象类中的抽象方法必须被子类实现:
352 |
353 | ```ts
354 | abstract class Animal {
355 | public name;
356 | public constructor(name) {
357 | this.name = name;
358 | }
359 | public abstract sayHi();
360 | }
361 |
362 | class Cat extends Animal {
363 | public eat() {
364 | console.log(`${this.name} is eating.`);
365 | }
366 | }
367 |
368 | let cat = new Cat('Tom');
369 |
370 | // index.ts(9,7): error TS2515: Non-abstract class 'Cat' does not implement inherited abstract member 'sayHi' from class 'Animal'.
371 | ```
372 |
373 | 上面的例子中,我们定义了一个类 `Cat` 继承了抽象类 `Animal`,但是没有实现抽象方法 `sayHi`,所以编译报错了。
374 |
375 | 下面是一个正确使用抽象类的例子:
376 |
377 | ```ts
378 | abstract class Animal {
379 | public name;
380 | public constructor(name) {
381 | this.name = name;
382 | }
383 | public abstract sayHi();
384 | }
385 |
386 | class Cat extends Animal {
387 | public sayHi() {
388 | console.log(`Meow, My name is ${this.name}`);
389 | }
390 | }
391 |
392 | let cat = new Cat('Tom');
393 | ```
394 |
395 | 上面的例子中,我们实现了抽象方法 `sayHi`,编译通过了。
396 |
397 | 需要注意的是,即使是抽象方法,TypeScript 的编译结果中,仍然会存在这个类,上面的代码的编译结果是:
398 |
399 | ```js
400 | var __extends =
401 | (this && this.__extends) ||
402 | function (d, b) {
403 | for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
404 | function __() {
405 | this.constructor = d;
406 | }
407 | d.prototype = b === null ? Object.create(b) : ((__.prototype = b.prototype), new __());
408 | };
409 | var Animal = (function () {
410 | function Animal(name) {
411 | this.name = name;
412 | }
413 | return Animal;
414 | })();
415 | var Cat = (function (_super) {
416 | __extends(Cat, _super);
417 | function Cat() {
418 | _super.apply(this, arguments);
419 | }
420 | Cat.prototype.sayHi = function () {
421 | console.log('Meow, My name is ' + this.name);
422 | };
423 | return Cat;
424 | })(Animal);
425 | var cat = new Cat('Tom');
426 | ```
427 |
428 | ## 类的类型
429 |
430 | 给类加上 TypeScript 的类型很简单,与接口类似:
431 |
432 | ```ts
433 | class Animal {
434 | name: string;
435 | constructor(name: string) {
436 | this.name = name;
437 | }
438 | sayHi(): string {
439 | return `My name is ${this.name}`;
440 | }
441 | }
442 |
443 | let a: Animal = new Animal('Jack');
444 | console.log(a.sayHi()); // My name is Jack
445 | ```
446 |
447 | ## 参考
448 |
449 | - [Classes](http://www.typescriptlang.org/docs/handbook/classes.html)([中文版](https://zhongsp.gitbooks.io/typescript-handbook/content/doc/handbook/Classes.html))
450 | - [ECMAScript 6 入门 - Class]
451 |
452 | [ecmascript 6 入门 - class]: http://es6.ruanyifeng.com/#docs/class
453 |
--------------------------------------------------------------------------------
/engineering/lint.md:
--------------------------------------------------------------------------------
1 | # 代码检查
2 |
3 | 2019 年 1 月,[TypeScirpt 官方决定全面采用 ESLint](https://www.oschina.net/news/103818/future-typescript-eslint) 作为代码检查的工具,并创建了一个新项目 [typescript-eslint][],提供了 TypeScript 文件的解析器 [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/parser) 和相关的配置选项 [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin) 等。而之前的两个 lint 解决方案都将弃用:
4 |
5 | - [typescript-eslint-parser](https://github.com/eslint/typescript-eslint-parser) 已停止维护
6 | - [TSLint](https://palantir.github.io/tslint/) 将提供迁移工具,并在 typescript-eslint 的功能足够完整后停止维护 TSLint(Once we consider ESLint feature-complete w.r.t. TSLint, we will deprecate TSLint and help users migrate to ESLint[1](https://medium.com/palantir/tslint-in-2019-1a144c2317a9))
7 |
8 | 综上所述,目前以及将来的 TypeScript 的代码检查方案就是 [typescript-eslint][]。
9 |
10 | ## 什么是代码检查
11 |
12 | 代码检查主要是用来发现代码错误、统一代码风格。
13 |
14 | 在 JavaScript 项目中,我们一般使用 [ESLint][] 来进行代码检查,它通过插件化的特性极大的丰富了适用范围,搭配 [typescript-eslint][] 之后,甚至可以用来检查 TypeScript 代码。
15 |
16 | ## 为什么需要代码检查
17 |
18 | 有人会觉得,JavaScript 非常灵活,所以需要代码检查。而 TypeScript 已经能够在编译阶段检查出很多问题了,为什么还需要代码检查呢?
19 |
20 | 因为 TypeScript 关注的重心是类型的检查,而不是代码风格。当团队的人员越来越多时,同样的逻辑不同的人写出来可能会有很大的区别:
21 |
22 | - 缩进应该是四个空格还是两个空格?
23 | - 是否应该禁用 `var`?
24 | - 接口名是否应该以 `I` 开头?
25 | - 是否应该强制使用 `===` 而不是 `==`?
26 |
27 | 这些问题 TypeScript 不会关注,但是却影响到多人协作开发时的效率、代码的可理解性以及可维护性。
28 |
29 | 下面来看一个具体的例子:
30 |
31 | ```ts
32 | var myName = 'Tom';
33 |
34 | console.log(`My name is ${myNane}`);
35 | console.log(`My name is ${myName.toStrng()}`);
36 | ```
37 |
38 | 以上代码你能看出有什么错误吗?
39 |
40 | 分别用 tsc 编译和 eslint 检查后,报错信息如下:
41 |
42 | ```ts
43 | var myName = 'Tom';
44 | // eslint 报错信息:
45 | // Unexpected var, use let or const instead.eslint(no-var)
46 |
47 | console.log(`My name is ${myNane}`);
48 | // tsc 报错信息:
49 | // Cannot find name 'myNane'. Did you mean 'myName'?
50 | // eslint 报错信息:
51 | // 'myNane' is not defined.eslint(no-undef)
52 | console.log(`My name is ${myName.toStrng()}`);
53 | // tsc 报错信息:
54 | // Property 'toStrng' does not exist on type 'string'. Did you mean 'toString'?
55 | ```
56 |
57 | | 存在的问题 | `tsc` 是否报错 | `eslint` 是否报错 |
58 | | --- | --- | --- |
59 | | 应该使用 `let` 或 `const` 而不是 `var` | ❌ | ✅ |
60 | | `myName` 被误写成了 `myNane` | ✅ | ✅ |
61 | | `toString` 被误写成了 `toStrng` | ✅️ | ❌ |
62 |
63 | 上例中,我们使用了 `var` 来定义一个变量,但其实 ES6 中有更先进的语法 `let` 和 `const`,此时就可以通过 `eslint` 检查出来,提示我们应该使用 `let` 或 `const` 而不是 `var`。
64 |
65 | 对于未定义的变量 `myNane`,`tsc` 和 `eslint` 都可以检查出来。
66 |
67 | 由于 `eslint` 无法识别 `myName` 存在哪些方法,所以对于拼写错误的 `toString` 没有检查出来。
68 |
69 | 由此可见,`eslint` 能够发现出一些 `tsc` 不会关心的错误,检查出一些潜在的问题,所以代码检查还是非常重要的。
70 |
71 | ## 在 TypeScript 中使用 ESLint
72 |
73 | ### 安装 ESLint
74 |
75 | ESLint 可以安装在当前项目中或全局环境下,因为代码检查是项目的重要组成部分,所以我们一般会将它安装在当前项目中。可以运行下面的脚本来安装:
76 |
77 | ```bash
78 | npm install --save-dev eslint
79 | ```
80 |
81 | 由于 ESLint 默认使用 [Espree](https://github.com/eslint/espree) 进行语法解析,无法识别 TypeScript 的一些语法,故我们需要安装 [`@typescript-eslint/parser`](https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/parser),替代掉默认的解析器,别忘了同时安装 `typescript`:
82 |
83 | ```bash
84 | npm install --save-dev typescript @typescript-eslint/parser
85 | ```
86 |
87 | 接下来需要安装对应的插件 [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin) 它作为 eslint 默认规则的补充,提供了一些额外的适用于 ts 语法的规则。
88 |
89 | ```bash
90 | npm install --save-dev @typescript-eslint/eslint-plugin
91 | ```
92 |
93 | ### 创建配置文件
94 |
95 | ESLint 需要一个配置文件来决定对哪些规则进行检查,配置文件的名称一般是 `.eslintrc.js` 或 `.eslintrc.json`。
96 |
97 | 当运行 ESLint 的时候检查一个文件的时候,它会首先尝试读取该文件的目录下的配置文件,然后再一级一级往上查找,将所找到的配置合并起来,作为当前被检查文件的配置。
98 |
99 | 我们在项目的根目录下创建一个 `.eslintrc.js`,内容如下:
100 |
101 | ```js
102 | module.exports = {
103 | parser: '@typescript-eslint/parser',
104 | plugins: ['@typescript-eslint'],
105 | rules: {
106 | // 禁止使用 var
107 | 'no-var': "error",
108 | // 优先使用 interface 而不是 type
109 | '@typescript-eslint/consistent-type-definitions': [
110 | "error",
111 | "interface"
112 | ]
113 | }
114 | }
115 | ```
116 |
117 | 以上配置中,我们指定了两个规则,其中 `no-var` 是 ESLint 原生的规则,`@typescript-eslint/consistent-type-definitions` 是 `@typescript-eslint/eslint-plugin` 新增的规则。
118 |
119 | 规则的取值一般是一个数组(上例中的 `@typescript-eslint/consistent-type-definitions`),其中第一项是 `off`、`warn` 或 `error` 中的一个,表示关闭、警告和报错。后面的项都是该规则的其他配置。
120 |
121 | 如果没有其他配置的话,则可以将规则的取值简写为数组中的第一项(上例中的 `no-var`)。
122 |
123 | 关闭、警告和报错的含义如下:
124 |
125 | - 关闭:禁用此规则
126 | - 警告:代码检查时输出错误信息,但是不会影响到 exit code
127 | - 报错:发现错误时,不仅会输出错误信息,而且 exit code 将被设为 1(一般 exit code 不为 0 则表示执行出现错误)
128 |
129 | ### 检查一个 ts 文件
130 |
131 | 创建了配置文件之后,我们来创建一个 ts 文件看看是否能用 ESLint 去检查它。
132 |
133 | 创建一个新文件 `index.ts`,将以下内容复制进去:
134 |
135 | ```ts
136 | var myName = 'Tom';
137 |
138 | type Foo = {};
139 | ```
140 |
141 | 然后执行以下命令:
142 |
143 | ```bash
144 | ./node_modules/.bin/eslint index.ts
145 | ```
146 |
147 | 则会得到如下报错信息:
148 |
149 | ```bash
150 | /path/to/index.ts
151 | 1:1 error Unexpected var, use let or const instead no-var
152 | 3:6 error Use an `interface` instead of a `type` @typescript-eslint/consistent-type-definitions
153 |
154 | ✖ 2 problems (2 errors, 0 warnings)
155 | 2 errors and 0 warnings potentially fixable with the `--fix` option.
156 | ```
157 |
158 | 上面的结果显示,刚刚配置的两个规则都生效了:禁止使用 `var`;优先使用 `interface` 而不是 `type`。
159 |
160 | 需要注意的是,我们使用的是 `./node_modules/.bin/eslint`,而不是全局的 `eslint` 脚本,这是因为代码检查是项目的重要组成部分,所以我们一般会将它安装在当前项目中。
161 |
162 | 可是每次执行这么长一段脚本颇有不便,我们可以通过在 `package.json` 中添加一个 `script` 来创建一个 npm script 来简化这个步骤:
163 |
164 | ```json
165 | {
166 | "scripts": {
167 | "eslint": "eslint index.ts"
168 | }
169 | }
170 | ```
171 |
172 | 这时只需执行 `npm run eslint` 即可。
173 |
174 | ### 检查整个项目的 ts 文件
175 |
176 | 我们的项目源文件一般是放在 `src` 目录下,所以需要将 `package.json` 中的 `eslint` 脚本改为对一个目录进行检查。由于 `eslint` 默认不会检查 `.ts` 后缀的文件,所以需要加上参数 `--ext .ts`:
177 |
178 | ```json
179 | {
180 | "scripts": {
181 | "eslint": "eslint src --ext .ts"
182 | }
183 | }
184 | ```
185 |
186 | 此时执行 `npm run eslint` 即会检查 `src` 目录下的所有 `.ts` 后缀的文件。
187 |
188 | ### 在 VSCode 中集成 ESLint 检查
189 |
190 | 在编辑器中集成 ESLint 检查,可以在开发过程中就发现错误,甚至可以在保存时自动修复错误,极大的增加了开发效率。
191 |
192 | 要在 VSCode 中集成 ESLint 检查,我们需要先安装 ESLint 插件,点击「扩展」按钮,搜索 ESLint,然后安装即可。
193 |
194 | 通过配置 VSCode,可以开启保存时自动修复的功能:
195 |
196 | ```json
197 | {
198 | "eslint.autoFixOnSave": true,
199 | "eslint.validate": [
200 | "javascript",
201 | "javascriptreact",
202 | {
203 | "language": "typescript",
204 | "autoFix": true
205 | },
206 | ],
207 | "typescript.tsdk": "node_modules/typescript/lib"
208 | }
209 | ```
210 |
211 | 就可以在保存文件后,自动修复为:
212 |
213 | ```ts
214 | let myName = 'Tom';
215 |
216 | interface Foo {}
217 | ```
218 |
219 | ### 使用 Prettier 修复格式错误
220 |
221 | ESLint 包含了一些代码格式的检查,比如空格、分号等。但前端社区中有一个更先进的工具可以用来格式化代码,那就是 [Prettier](https://prettier.io/)。
222 |
223 | Prettier 聚焦于代码的格式化,通过语法分析,重新整理代码的格式,让所有人的代码都保持同样的风格。
224 |
225 | 首先需要安装 Prettier:
226 |
227 | ```bash
228 | npm install --save-dev prettier
229 | ```
230 |
231 | 然后创建一个 `prettier.config.js` 文件,里面包含 Prettier 的配置项。Prettier 的配置项很少,这里我推荐大家一个配置规则,作为参考:
232 |
233 | ```js
234 | // prettier.config.js or .prettierrc.js
235 | module.exports = {
236 | // 一行最多 100 字符
237 | printWidth: 100,
238 | // 使用 4 个空格缩进
239 | tabWidth: 4,
240 | // 不使用缩进符,而使用空格
241 | useTabs: false,
242 | // 行尾需要有分号
243 | semi: true,
244 | // 使用单引号
245 | singleQuote: true,
246 | // 对象的 key 仅在必要时用引号
247 | quoteProps: 'as-needed',
248 | // jsx 不使用单引号,而使用双引号
249 | jsxSingleQuote: false,
250 | // 末尾不需要逗号
251 | trailingComma: 'none',
252 | // 大括号内的首尾需要空格
253 | bracketSpacing: true,
254 | // jsx 标签的反尖括号需要换行
255 | jsxBracketSameLine: false,
256 | // 箭头函数,只有一个参数的时候,也需要括号
257 | arrowParens: 'always',
258 | // 每个文件格式化的范围是文件的全部内容
259 | rangeStart: 0,
260 | rangeEnd: Infinity,
261 | // 不需要写文件开头的 @prettier
262 | requirePragma: false,
263 | // 不需要自动在文件开头插入 @prettier
264 | insertPragma: false,
265 | // 使用默认的折行标准
266 | proseWrap: 'preserve',
267 | // 根据显示样式决定 html 要不要折行
268 | htmlWhitespaceSensitivity: 'css',
269 | // 换行符使用 lf
270 | endOfLine: 'lf'
271 | };
272 | ```
273 |
274 | 接下来安装 VSCode 中的 Prettier 插件,然后修改 `.vscode/settings.json`:
275 |
276 | ```json
277 | {
278 | "files.eol": "\n",
279 | "editor.tabSize": 4,
280 | "editor.formatOnSave": true,
281 | "editor.defaultFormatter": "esbenp.prettier-vscode",
282 | "eslint.autoFixOnSave": true,
283 | "eslint.validate": [
284 | "javascript",
285 | "javascriptreact",
286 | {
287 | "language": "typescript",
288 | "autoFix": true
289 | }
290 | ],
291 | "typescript.tsdk": "node_modules/typescript/lib"
292 | }
293 | ```
294 |
295 | 这样就实现了保存文件时自动格式化并且自动修复 ESLint 错误。
296 |
297 | 需要注意的是,由于 ESLint 也可以检查一些代码格式的问题,所以在和 Prettier 配合使用时,我们一般会把 ESLint 中的代码格式相关的规则禁用掉,否则就会有冲突了。
298 |
299 | ### 使用 AlloyTeam 的 ESLint 配置
300 |
301 | ESLint 原生的规则和 `@typescript-eslint/eslint-plugin` 的规则太多了,而且原生的规则有一些在 TypeScript 中支持的不好,需要禁用掉。
302 |
303 | 这里我推荐使用 [AlloyTeam ESLint 规则中的 TypeScript 版本](https://github.com/AlloyTeam/eslint-config-alloy#typescript),它已经为我们提供了一套完善的配置规则,并且与 Prettier 是完全兼容的(eslint-config-alloy 不包含任何代码格式的规则,代码格式的问题交给更专业的 Prettier 去处理)。
304 |
305 | 安装:
306 |
307 | ```bash
308 | npm install --save-dev eslint typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-config-alloy
309 | ```
310 |
311 | 在你的项目根目录下创建 `.eslintrc.js`,并将以下内容复制到文件中即可:
312 |
313 | ```js
314 | module.exports = {
315 | extends: [
316 | 'alloy',
317 | 'alloy/typescript',
318 | ],
319 | env: {
320 | // 您的环境变量(包含多个预定义的全局变量)
321 | // Your environments (which contains several predefined global variables)
322 | //
323 | // browser: true,
324 | // node: true,
325 | // mocha: true,
326 | // jest: true,
327 | // jquery: true
328 | },
329 | globals: {
330 | // 您的全局变量(设置为 false 表示它不允许被重新赋值)
331 | // Your global variables (setting to false means it's not allowed to be reassigned)
332 | //
333 | // myGlobal: false
334 | },
335 | rules: {
336 | // 自定义您的规则
337 | // Customize your rules
338 | }
339 | };
340 | ```
341 |
342 | 更多的使用方法,请参考 [AlloyTeam ESLint 规则](https://github.com/AlloyTeam/eslint-config-alloy)
343 |
344 | ### 使用 ESLint 检查 tsx 文件
345 |
346 | 如果需要同时支持对 tsx 文件的检查,则需要对以上步骤做一些调整:
347 |
348 | #### 安装 `eslint-plugin-react`
349 |
350 | ```bash
351 | npm install --save-dev eslint-plugin-react
352 | ```
353 |
354 | #### package.json 中的 scripts.eslint 添加 `.tsx` 后缀
355 |
356 | ```json
357 | {
358 | "scripts": {
359 | "eslint": "eslint src --ext .ts,.tsx"
360 | }
361 | }
362 | ```
363 |
364 | #### VSCode 的配置中新增 typescriptreact 检查
365 |
366 | ```json
367 | {
368 | "files.eol": "\\n",
369 | "editor.tabSize": 4,
370 | "editor.formatOnSave": true,
371 | "editor.defaultFormatter": "esbenp.prettier-vscode",
372 | "eslint.autoFixOnSave": true,
373 | "eslint.validate": [
374 | "javascript",
375 | "javascriptreact",
376 | {
377 | "language": "typescript",
378 | "autoFix": true
379 | },
380 | {
381 | "language": "typescriptreact",
382 | "autoFix": true
383 | }
384 | ],
385 | "typescript.tsdk": "node_modules/typescript/lib"
386 | }
387 | ```
388 |
389 | #### 使用 AlloyTeam ESLint 规则中的 TypeScript React 版本
390 |
391 | [AlloyTeam ESLint 规则中的 TypeScript React 版本](https://github.com/AlloyTeam/eslint-config-alloy#typescript-react)
392 |
393 | ## Troubleshootings
394 |
395 | ### Cannot find module '@typescript-eslint/parser'
396 |
397 | 你运行的是全局的 eslint,需要改为运行 `./node_modules/.bin/eslint`。
398 |
399 | ### VSCode 没有显示出 ESLint 的报错
400 |
401 | 1. 检查「文件 => 首选项 => 设置」中有没有配置正确
402 | 2. 检查必要的 npm 包有没有安装
403 | 3. 检查 `.eslintrc.js` 有没有配置
404 | 4. 检查文件是不是在 `.eslintignore` 中
405 |
406 | 如果以上步骤都不奏效,则可以在「文件 => 首选项 => 设置」中配置 `"eslint.trace.server": "messages"`,按 `Ctrl`+`Shift`+`U` 打开输出面板,然后选择 ESLint 输出,查看具体错误。
407 |
408 | 
409 |
410 | ### 为什么有些定义了的变量(比如使用 `enum` 定义的变量)未使用,ESLint 却没有报错?
411 |
412 | 因为无法支持这种变量定义的检查。建议在 `tsconfig.json` 中添加以下配置,使 `tsc` 编译过程能够检查出定义了未使用的变量:
413 |
414 | ```json
415 | {
416 | "compilerOptions": {
417 | "noUnusedLocals": true,
418 | "noUnusedParameters": true
419 | }
420 | }
421 | ```
422 |
423 | ### 启用了 noUnusedParameters 之后,只使用了第二个参数,但是又必须传入第一个参数,这就会报错了
424 |
425 | 第一个参数以下划线开头即可,参考 https://github.com/Microsoft/TypeScript/issues/9458
426 |
427 | [ESLint]: https://eslint.org/
428 | [typescript-eslint]: https://github.com/typescript-eslint/typescript-eslint
429 |
--------------------------------------------------------------------------------
/basics/type-assertion.md:
--------------------------------------------------------------------------------
1 | # 类型断言
2 |
3 | 类型断言(Type Assertion)可以用来手动指定一个值的类型。
4 |
5 | ## 语法
6 |
7 | ```ts
8 | 值 as 类型
9 | ```
10 |
11 | 或
12 |
13 | ```ts
14 | <类型>值
15 | ```
16 |
17 | 在 tsx 语法(React 的 jsx 语法的 ts 版)中必须使用前者,即 `值 as 类型`。
18 |
19 | 形如 `` 的语法在 tsx 中表示的是一个 `ReactNode`,在 ts 中除了表示类型断言之外,也可能是表示一个[泛型][]。
20 |
21 | 故建议大家在使用类型断言时,统一使用 `值 as 类型` 这样的语法,本书中也会贯彻这一思想。
22 |
23 | ## 类型断言的用途
24 |
25 | 类型断言的常见用途有以下几种:
26 |
27 | ### 将一个联合类型断言为其中一个类型
28 |
29 | [之前提到过](union-types.md#访问联合类型的属性或方法),当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们**只能访问此联合类型的所有类型中共有的属性或方法**:
30 |
31 | ```ts
32 | interface Cat {
33 | name: string;
34 | run(): void;
35 | }
36 | interface Fish {
37 | name: string;
38 | swim(): void;
39 | }
40 |
41 | function getName(animal: Cat | Fish) {
42 | return animal.name;
43 | }
44 | ```
45 |
46 | 而有时候,我们确实需要在还不确定类型的时候就访问其中一个类型特有的属性或方法,比如:
47 |
48 | ```ts
49 | interface Cat {
50 | name: string;
51 | run(): void;
52 | }
53 | interface Fish {
54 | name: string;
55 | swim(): void;
56 | }
57 |
58 | function isFish(animal: Cat | Fish) {
59 | if (typeof animal.swim === 'function') {
60 | return true;
61 | }
62 | return false;
63 | }
64 |
65 | // index.ts:11:23 - error TS2339: Property 'swim' does not exist on type 'Cat | Fish'.
66 | // Property 'swim' does not exist on type 'Cat'.
67 | ```
68 |
69 | 上面的例子中,获取 `animal.swim` 的时候会报错。
70 |
71 | 此时可以使用类型断言,将 `animal` 断言成 `Fish`:
72 |
73 | ```ts
74 | interface Cat {
75 | name: string;
76 | run(): void;
77 | }
78 | interface Fish {
79 | name: string;
80 | swim(): void;
81 | }
82 |
83 | function isFish(animal: Cat | Fish) {
84 | if (typeof (animal as Fish).swim === 'function') {
85 | return true;
86 | }
87 | return false;
88 | }
89 | ```
90 |
91 | 这样就可以解决访问 `animal.swim` 时报错的问题了。
92 |
93 | 需要注意的是,类型断言只能够「欺骗」TypeScript 编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误:
94 |
95 | ```ts
96 | interface Cat {
97 | name: string;
98 | run(): void;
99 | }
100 | interface Fish {
101 | name: string;
102 | swim(): void;
103 | }
104 |
105 | function swim(animal: Cat | Fish) {
106 | (animal as Fish).swim();
107 | }
108 |
109 | const tom: Cat = {
110 | name: 'Tom',
111 | run() { console.log('run') }
112 | };
113 | swim(tom);
114 | // Uncaught TypeError: animal.swim is not a function`
115 | ```
116 |
117 | 上面的例子编译时不会报错,但在运行时会报错:
118 |
119 | ```text
120 | Uncaught TypeError: animal.swim is not a function`
121 | ```
122 |
123 | 原因是 `(animal as Fish).swim()` 这段代码隐藏了 `animal` 可能为 `Cat` 的情况,将 `animal` 直接断言为 `Fish` 了,而 TypeScript 编译器信任了我们的断言,故在调用 `swim()` 时没有编译错误。
124 |
125 | 可是 `swim` 函数接受的参数是 `Cat | Fish`,一旦传入的参数是 `Cat` 类型的变量,由于 `Cat` 上没有 `swim` 方法,就会导致运行时错误了。
126 |
127 | 总之,使用类型断言时一定要格外小心,尽量避免断言后调用方法或引用深层属性,以减少不必要的运行时错误。
128 |
129 | ### 将一个父类断言为更加具体的子类
130 |
131 | 当类之间有继承关系时,类型断言也是很常见的:
132 |
133 | ```ts
134 | class ApiError extends Error {
135 | code: number = 0;
136 | }
137 | class HttpError extends Error {
138 | statusCode: number = 200;
139 | }
140 |
141 | function isApiError(error: Error) {
142 | if (typeof (error as ApiError).code === 'number') {
143 | return true;
144 | }
145 | return false;
146 | }
147 | ```
148 |
149 | 上面的例子中,我们声明了函数 `isApiError`,它用来判断传入的参数是不是 `ApiError` 类型,为了实现这样一个函数,它的参数的类型肯定得是比较抽象的父类 `Error`,这样的话这个函数就能接受 `Error` 或它的子类作为参数了。
150 |
151 | 但是由于父类 `Error` 中没有 `code` 属性,故直接获取 `error.code` 会报错,需要使用类型断言获取 `(error as ApiError).code`。
152 |
153 | 大家可能会注意到,在这个例子中有一个更合适的方式来判断是不是 `ApiError`,那就是使用 `instanceof`:
154 |
155 | ```ts
156 | class ApiError extends Error {
157 | code: number = 0;
158 | }
159 | class HttpError extends Error {
160 | statusCode: number = 200;
161 | }
162 |
163 | function isApiError(error: Error) {
164 | if (error instanceof ApiError) {
165 | return true;
166 | }
167 | return false;
168 | }
169 | ```
170 |
171 | 上面的例子中,确实使用 `instanceof` 更加合适,因为 `ApiError` 是一个 JavaScript 的类,能够通过 `instanceof` 来判断 `error` 是否是它的实例。
172 |
173 | 但是有的情况下 `ApiError` 和 `HttpError` 不是一个真正的类,而只是一个 TypeScript 的接口(`interface`),接口是一个类型,不是一个真正的值,它在编译结果中会被删除,当然就无法使用 `instanceof` 来做运行时判断了:
174 |
175 | ```ts
176 | interface ApiError extends Error {
177 | code: number;
178 | }
179 | interface HttpError extends Error {
180 | statusCode: number;
181 | }
182 |
183 | function isApiError(error: Error) {
184 | if (error instanceof ApiError) {
185 | return true;
186 | }
187 | return false;
188 | }
189 |
190 | // index.ts:9:26 - error TS2693: 'ApiError' only refers to a type, but is being used as a value here.
191 | ```
192 |
193 | 此时就只能用类型断言,通过判断是否存在 `code` 属性,来判断传入的参数是不是 `ApiError` 了:
194 |
195 | ```ts
196 | interface ApiError extends Error {
197 | code: number;
198 | }
199 | interface HttpError extends Error {
200 | statusCode: number;
201 | }
202 |
203 | function isApiError(error: Error) {
204 | if (typeof (error as ApiError).code === 'number') {
205 | return true;
206 | }
207 | return false;
208 | }
209 | ```
210 |
211 | ### 将任何一个类型断言为 `any`
212 |
213 | 理想情况下,TypeScript 的类型系统运转良好,每个值的类型都具体而精确。
214 |
215 | 当我们引用一个在此类型上不存在的属性或方法时,就会报错:
216 |
217 | ```ts
218 | const foo: number = 1;
219 | foo.length = 1;
220 |
221 | // index.ts:2:5 - error TS2339: Property 'length' does not exist on type 'number'.
222 | ```
223 |
224 | 上面的例子中,数字类型的变量 `foo` 上是没有 `length` 属性的,故 TypeScript 给出了相应的错误提示。
225 |
226 | 这种错误提示显然是非常有用的。
227 |
228 | 但有的时候,我们非常确定这段代码不会出错,比如下面这个例子:
229 |
230 | ```ts
231 | window.foo = 1;
232 |
233 | // index.ts:1:8 - error TS2339: Property 'foo' does not exist on type 'Window & typeof globalThis'.
234 | ```
235 |
236 | 上面的例子中,我们需要将 `window` 上添加一个属性 `foo`,但 TypeScript 编译时会报错,提示我们 `window` 上不存在 `foo` 属性。
237 |
238 | 此时我们可以使用 `as any` 临时将 `window` 断言为 `any` 类型:
239 |
240 | ```ts
241 | (window as any).foo = 1;
242 | ```
243 |
244 | 在 `any` 类型的变量上,访问任何属性都是允许的。
245 |
246 | 需要注意的是,将一个变量断言为 `any` 可以说是解决 TypeScript 中类型问题的最后一个手段。
247 |
248 | **它极有可能掩盖了真正的类型错误,所以如果不是非常确定,就不要使用 `as any`。**
249 |
250 | 上面的例子中,我们也可以通过[扩展 window 的类型(TODO)][]解决这个错误,不过如果只是临时的增加 `foo` 属性,`as any` 会更加方便。
251 |
252 | 总之,**一方面不能滥用 `as any`,另一方面也不要完全否定它的作用,我们需要在类型的严格性和开发的便利性之间掌握平衡**(这也是 [TypeScript 的设计理念][]之一),才能发挥出 TypeScript 最大的价值。
253 |
254 | ### 将 `any` 断言为一个具体的类型
255 |
256 | 在日常的开发中,我们不可避免的需要处理 `any` 类型的变量,它们可能是由于第三方库未能定义好自己的类型,也有可能是历史遗留的或其他人编写的烂代码,还可能是受到 TypeScript 类型系统的限制而无法精确定义类型的场景。
257 |
258 | 遇到 `any` 类型的变量时,我们可以选择无视它,任由它滋生更多的 `any`。
259 |
260 | 我们也可以选择改进它,通过类型断言及时的把 `any` 断言为精确的类型,亡羊补牢,使我们的代码向着高可维护性的目标发展。
261 |
262 | 举例来说,历史遗留的代码中有个 `getCacheData`,它的返回值是 `any`:
263 |
264 | ```ts
265 | function getCacheData(key: string): any {
266 | return (window as any).cache[key];
267 | }
268 | ```
269 |
270 | 那么我们在使用它时,最好能够将调用了它之后的返回值断言成一个精确的类型,这样就方便了后续的操作:
271 |
272 | ```ts
273 | function getCacheData(key: string): any {
274 | return (window as any).cache[key];
275 | }
276 |
277 | interface Cat {
278 | name: string;
279 | run(): void;
280 | }
281 |
282 | const tom = getCacheData('tom') as Cat;
283 | tom.run();
284 | ```
285 |
286 | 上面的例子中,我们调用完 `getCacheData` 之后,立即将它断言为 `Cat` 类型。这样的话明确了 `tom` 的类型,后续对 `tom` 的访问时就有了代码补全,提高了代码的可维护性。
287 |
288 | ## 类型断言的限制
289 |
290 | > 本小节的前置知识点:[结构类型系统(TODO)][]、[类型兼容性(TODO)][]
291 |
292 | 从上面的例子中,我们可以总结出:
293 |
294 | - 联合类型可以被断言为其中一个类型
295 | - 父类可以被断言为子类
296 | - 任何类型都可以被断言为 any
297 | - any 可以被断言为任何类型
298 |
299 | 那么类型断言有没有什么限制呢?是不是任何一个类型都可以被断言为任何另一个类型呢?
300 |
301 | 答案是否定的——并不是任何一个类型都可以被断言为任何另一个类型。
302 |
303 | 具体来说,若 `A` 兼容 `B`,那么 `A` 能够被断言为 `B`,`B` 也能被断言为 `A`。
304 |
305 | 下面我们通过一个简化的例子,来理解类型断言的限制:
306 |
307 | ```ts
308 | interface Animal {
309 | name: string;
310 | }
311 | interface Cat {
312 | name: string;
313 | run(): void;
314 | }
315 |
316 | let tom: Cat = {
317 | name: 'Tom',
318 | run: () => { console.log('run') }
319 | };
320 | let animal: Animal = tom;
321 | ```
322 |
323 | 我们知道,TypeScript 是结构类型系统,类型之间的对比只会比较它们最终的结构,而会忽略它们定义时的关系。
324 |
325 | 在上面的例子中,`Cat` 包含了 `Animal` 中的所有属性,除此之外,它还有一个额外的方法 `run`。TypeScript 并不关心 `Cat` 和 `Animal` 之间定义时是什么关系,而只会看它们最终的结构有什么关系——所以它与 `Cat extends Animal` 是等价的:
326 |
327 | ```ts
328 | interface Animal {
329 | name: string;
330 | }
331 | interface Cat extends Animal {
332 | run(): void;
333 | }
334 | ```
335 |
336 | 那么也不难理解为什么 `Cat` 类型的 `tom` 可以赋值给 `Animal` 类型的 `animal` 了——就像面向对象编程中我们可以将子类的实例赋值给类型为父类的变量。
337 |
338 | 我们把它换成 TypeScript 中更专业的说法,即:`Animal` 兼容 `Cat`。
339 |
340 | 当 `Animal` 兼容 `Cat` 时,它们就可以互相进行类型断言了:
341 |
342 | ```ts
343 | interface Animal {
344 | name: string;
345 | }
346 | interface Cat {
347 | name: string;
348 | run(): void;
349 | }
350 |
351 | function testAnimal(animal: Animal) {
352 | return (animal as Cat);
353 | }
354 | function testCat(cat: Cat) {
355 | return (cat as Animal);
356 | }
357 | ```
358 |
359 | 这样的设计其实也很容易就能理解:
360 |
361 | - 允许 `animal as Cat` 是因为「父类可以被断言为子类」,这个前面已经学习过了
362 | - 允许 `cat as Animal` 是因为既然子类拥有父类的属性和方法,那么被断言为父类,获取父类的属性、调用父类的方法,就不会有任何问题,故「子类可以被断言为父类」
363 |
364 | 需要注意的是,这里我们使用了简化的父类子类的关系来表达类型的兼容性,而实际上 TypeScript 在判断类型的兼容性时,比这种情况复杂很多,详细请参考[类型的兼容性(TODO)][]章节。
365 |
366 | 总之,若 `A` 兼容 `B`,那么 `A` 能够被断言为 `B`,`B` 也能被断言为 `A`。
367 |
368 | 同理,若 `B` 兼容 `A`,那么 `A` 能够被断言为 `B`,`B` 也能被断言为 `A`。
369 |
370 | 所以这也可以换一种说法:
371 |
372 | 要使得 `A` 能够被断言为 `B`,只需要 `A` 兼容 `B` 或 `B` 兼容 `A` 即可,这也是为了在类型断言时的安全考虑,毕竟毫无根据的断言是非常危险的。
373 |
374 | 综上所述:
375 |
376 | - 联合类型可以被断言为其中一个类型
377 | - 父类可以被断言为子类
378 | - 任何类型都可以被断言为 any
379 | - any 可以被断言为任何类型
380 | - 要使得 `A` 能够被断言为 `B`,只需要 `A` 兼容 `B` 或 `B` 兼容 `A` 即可
381 |
382 | 其实前四种情况都是最后一个的特例。
383 |
384 | ## 双重断言
385 |
386 | 既然:
387 |
388 | - 任何类型都可以被断言为 any
389 | - any 可以被断言为任何类型
390 |
391 | 那么我们是不是可以使用双重断言 `as any as Foo` 来将任何一个类型断言为任何另一个类型呢?
392 |
393 | ```ts
394 | interface Cat {
395 | run(): void;
396 | }
397 | interface Fish {
398 | swim(): void;
399 | }
400 |
401 | function testCat(cat: Cat) {
402 | return (cat as any as Fish);
403 | }
404 | ```
405 |
406 | 在上面的例子中,若直接使用 `cat as Fish` 肯定会报错,因为 `Cat` 和 `Fish` 互相都不兼容。
407 |
408 | 但是若使用双重断言,则可以打破「要使得 `A` 能够被断言为 `B`,只需要 `A` 兼容 `B` 或 `B` 兼容 `A` 即可」的限制,将任何一个类型断言为任何另一个类型。
409 |
410 | 若你使用了这种双重断言,那么十有八九是非常错误的,它很可能会导致运行时错误。
411 |
412 | **除非迫不得已,千万别用双重断言。**
413 |
414 | ## 类型断言 vs 类型转换
415 |
416 | 类型断言只会影响 TypeScript 编译时的类型,类型断言语句在编译结果中会被删除:
417 |
418 | ```ts
419 | function toBoolean(something: any): boolean {
420 | return something as boolean;
421 | }
422 |
423 | toBoolean(1);
424 | // 返回值为 1
425 | ```
426 |
427 | 在上面的例子中,将 `something` 断言为 `boolean` 虽然可以通过编译,但是并没有什么用,代码在编译后会变成:
428 |
429 | ```js
430 | function toBoolean(something) {
431 | return something;
432 | }
433 |
434 | toBoolean(1);
435 | // 返回值为 1
436 | ```
437 |
438 | 所以类型断言不是类型转换,它不会真的影响到变量的类型。
439 |
440 | 若要进行类型转换,需要直接调用类型转换的方法:
441 |
442 | ```ts
443 | function toBoolean(something: any): boolean {
444 | return Boolean(something);
445 | }
446 |
447 | toBoolean(1);
448 | // 返回值为 true
449 | ```
450 |
451 | ## 类型断言 vs 类型声明
452 |
453 | 在这个例子中:
454 |
455 | ```ts
456 | function getCacheData(key: string): any {
457 | return (window as any).cache[key];
458 | }
459 |
460 | interface Cat {
461 | name: string;
462 | run(): void;
463 | }
464 |
465 | const tom = getCacheData('tom') as Cat;
466 | tom.run();
467 | ```
468 |
469 | 我们使用 `as Cat` 将 `any` 类型断言为了 `Cat` 类型。
470 |
471 | 但实际上还有其他方式可以解决这个问题:
472 |
473 | ```ts
474 | function getCacheData(key: string): any {
475 | return (window as any).cache[key];
476 | }
477 |
478 | interface Cat {
479 | name: string;
480 | run(): void;
481 | }
482 |
483 | const tom: Cat = getCacheData('tom');
484 | tom.run();
485 | ```
486 |
487 | 上面的例子中,我们通过类型声明的方式,将 `tom` 声明为 `Cat`,然后再将 `any` 类型的 `getCacheData('tom')` 赋值给 `Cat` 类型的 `tom`。
488 |
489 | 这和类型断言是非常相似的,而且产生的结果也几乎是一样的——`tom` 在接下来的代码中都变成了 `Cat` 类型。
490 |
491 | 它们的区别,可以通过这个例子来理解:
492 |
493 | ```ts
494 | interface Animal {
495 | name: string;
496 | }
497 | interface Cat {
498 | name: string;
499 | run(): void;
500 | }
501 |
502 | const animal: Animal = {
503 | name: 'tom'
504 | };
505 | let tom = animal as Cat;
506 | ```
507 |
508 | 在上面的例子中,由于 `Animal` 兼容 `Cat`,故可以将 `animal` 断言为 `Cat` 赋值给 `tom`。
509 |
510 | 但是若直接声明 `tom` 为 `Cat` 类型:
511 |
512 | ```ts
513 | interface Animal {
514 | name: string;
515 | }
516 | interface Cat {
517 | name: string;
518 | run(): void;
519 | }
520 |
521 | const animal: Animal = {
522 | name: 'tom'
523 | };
524 | let tom: Cat = animal;
525 |
526 | // index.ts:12:5 - error TS2741: Property 'run' is missing in type 'Animal' but required in type 'Cat'.
527 | ```
528 |
529 | 则会报错,不允许将 `animal` 赋值为 `Cat` 类型的 `tom`。
530 |
531 | 这很容易理解,`Animal` 可以看作是 `Cat` 的父类,当然不能将父类的实例赋值给类型为子类的变量。
532 |
533 | 深入的讲,它们的核心区别就在于:
534 |
535 | - `animal` 断言为 `Cat`,只需要满足 `Animal` 兼容 `Cat` 或 `Cat` 兼容 `Animal` 即可
536 | - `animal` 赋值给 `tom`,需要满足 `Cat` 兼容 `Animal` 才行
537 |
538 | 但是 `Cat` 并不兼容 `Animal`。
539 |
540 | 而在前一个例子中,由于 `getCacheData('tom')` 是 `any` 类型,`any` 兼容 `Cat`,`Cat` 也兼容 `any`,故
541 |
542 | ```ts
543 | const tom = getCacheData('tom') as Cat;
544 | ```
545 |
546 | 等价于
547 |
548 | ```ts
549 | const tom: Cat = getCacheData('tom');
550 | ```
551 |
552 | 知道了它们的核心区别,就知道了类型声明是比类型断言更加严格的。
553 |
554 | 所以为了增加代码的质量,我们最好优先使用类型声明,这也比类型断言的 `as` 语法更加优雅。
555 |
556 | ## 类型断言 vs 泛型
557 |
558 | > 本小节的前置知识点:[泛型][]
559 |
560 | 还是这个例子:
561 |
562 | ```ts
563 | function getCacheData(key: string): any {
564 | return (window as any).cache[key];
565 | }
566 |
567 | interface Cat {
568 | name: string;
569 | run(): void;
570 | }
571 |
572 | const tom = getCacheData('tom') as Cat;
573 | tom.run();
574 | ```
575 |
576 | 我们还有第三种方式可以解决这个问题,那就是泛型:
577 |
578 | ```ts
579 | function getCacheData(key: string): T {
580 | return (window as any).cache[key];
581 | }
582 |
583 | interface Cat {
584 | name: string;
585 | run(): void;
586 | }
587 |
588 | const tom = getCacheData('tom');
589 | tom.run();
590 | ```
591 |
592 | 通过给 `getCacheData` 函数添加了一个泛型 ``,我们可以更加规范的实现对 `getCacheData` 返回值的约束,这也同时去除掉了代码中的 `any`,是最优的一个解决方案。
593 |
594 | ## 参考
595 |
596 | - [TypeScript Deep Dive / Type Assertion](https://basarat.gitbooks.io/typescript/content/docs/types/type-assertion.html)
597 | - [Advanced Types # Type Guards and Differentiating Types](http://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types)([中文版](https://zhongsp.gitbooks.io/typescript-handbook/content/doc/handbook/Advanced%20Types.html#类型保护与区分类型(type-guards-and-differentiating-types)))
598 | - [TypeScript 的设计理念][]
599 |
600 | [TypeScript 的设计理念]: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals
601 | [泛型]: ../advanced/generics.md
602 |
--------------------------------------------------------------------------------
/basics/declaration-files.md:
--------------------------------------------------------------------------------
1 | # 声明文件
2 |
3 | 当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
4 |
5 | ## 新语法索引
6 |
7 | 由于本章涉及大量新语法,故在本章开头列出新语法的索引,方便大家在使用这些新语法时能快速查找到对应的讲解:
8 |
9 | - [`declare var`](#declare-var) 声明全局变量
10 | - [`declare function`](#declare-function) 声明全局方法
11 | - [`declare class`](#declare-class) 声明全局类
12 | - [`declare enum`](#declare-enum) 声明全局枚举类型
13 | - [`declare namespace`](#declare-namespace) 声明(含有子属性的)全局对象
14 | - [`interface` 和 `type`](#interface-和-type) 声明全局类型
15 | - [`export`](#export) 导出变量
16 | - [`export namespace`](#export-namespace) 导出(含有子属性的)对象
17 | - [`export default`](#export-default) ES6 默认导出
18 | - [`export =`](#export-1) commonjs 导出模块
19 | - [`export as namespace`](#export-as-namespace) UMD 库声明全局变量
20 | - [`declare global`](#declare-global) 扩展全局变量
21 | - [`declare module`](#declare-module) 扩展模块
22 | - [`/// `](#san-xie-xian-zhi-ling) 三斜线指令
23 |
24 | ## 什么是声明语句
25 |
26 | 假如我们想使用第三方库 jQuery,一种常见的方式是在 html 中通过 `