沐光

记录在前端之路的点点滴滴

重习 typescript

前言

俗话说的好:“温故而知新”,以前学习 typescript 只是因为工作需要去了解,然后临时磨刀学了些皮毛,但缺没有对 typescript 有更深入的理解,因此今天趁工作之余,系统了解一下 typescript 的相关知识,为后续的学习做好铺垫。

注意:文章大部分为引用内容,且多为个人认为重要和难以记住的地方

了解 tsconfig 配置

做 ts 项目的时候,经常会发现项目内有个 tsconfig.json 的配置文件,那么这些配置是从哪里来的呢,它具体有那些配置属性呢,这里就一点点列举:

生成配置属性

1
2
3
4
5
6
7
8
# 自己进入一个空项目
yarn add typescript

# 在 package.json 的 script 内写上:
# "tsinit": "tsc --init"

# 生成 tsconfig.json
yarn tsinit

配置属性内容

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
{
"compilerOptions": {
/*
* Basic Options
*/
// "incremental": true, /* 是否增量编译 */
"target": "es5" /* 指定编译后的 ECMAScript 版本: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
"module": "commonjs" /* 指定使用的模块标准: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
// "lib": [], /* 指定编译时包含的库文件 */
// "allowJs": true, /* 指定是否允许编译 js 类型文件 */
// "checkJs": true, /* 指定是否检查和报告 js 文件中的错误,通常与 allowJS 一起使用 */
// "jsx": "preserve", /* 指定 jsx 代码用于的开发环境: 'preserve', 'react-native', or 'react' */
// "declaration": true, /* 是否在编译的时候生成相应的 ".d.ts" 声明文件,但是 declaration 和 allowJs 不能同时设为 true */
// "declarationMap": true, /* 是否为声明文件生成 map 文件 */
// "sourceMap": true, /* 编译时是否生成 map 文件 */
// "outFile": "./", /* 指定输出的文件,它的值为一个文件路径名,只有设置 module 的值为 amd 和 system 模块时才支持这个配置 */
// "outDir": "./", /* 指定输出文件夹 */
// "rootDir": "./", /* 指定编译文件的根目录 */
// "composite": true, /* 是否编译构建引用项目 */
// "tsBuildInfoFile": "./", /* 增量编译文件的存储位置 */
// "removeComments": true, /* 是否将编译后的文件中的注释删掉 */
// "noEmit": true, /* 不生成编译文件 */
// "importHelpers": true, /* 指定是否引入 tslib 里的辅助工具函数 */
// "downlevelIteration": true, /* 当 target 为 'ES5' 或 'ES3' 时,为 'for-of', 'spread' 和 'destructuring' 中的迭代器提供完全支持 */
// "isolatedModules": true, /* 指定是否将每个文件作为单独的模块(与“ts.transpileModule”类似) */

/*
* Strict Type-Checking Options
*/
"strict": true /* 开启所有严格的类型检查 */,
// "noImplicitAny": true, /* 禁止隐式的 any 类型 */
// "strictNullChecks": true, /* 不允许把 null、undefined 赋值给其他类型变置 */
// "strictFunctionTypes": true, /* 指定是否使用函数参数双向协变检查 */
// "strictBindCallApply": true, /* 对 bind、call 和 apply 绑定的方法的参数的检测是严格检测的 */
// "strictPropertyInitialization": true, /* 检查类的非 undefined 属性是否已经在构造函数里初始化,需要同时开启 strictNullChecks */
// "noImplicitThis": true, /* 禁止 this 的类型为 any */
// "alwaysStrict": true, /* 指定始终以严格模式检查每个模块,并且在编译之后的 js 文件中加入 "use strict" 字符串 */

/*
* Additional Checks
*/
// "noUnusedLocals": true, /* 用于检查是否有定义了但是没有使用的变量 */
// "noUnusedParameters": true, /* 用于检查是否有在函数体中没有使用的参数 */
// "noImplicitReturns": true, /* 用于检查函数是否有返回值 */
// "noFallthroughCasesInSwitch": true, /* 用于检查 switch 中是否有 case 没有使用 break 跳出 switch */

/*
* Module Resolution Options
*/
// "moduleResolution": "node", /* 模块解析策略: 'node' (Node.js) 或者 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* 设置解析非相对模块名称的基本目录 */
// "paths": {}, /* 设置模块名称到基于baseUrl的路径映射,类似于 webpack 的 alias 别名 */
// "rootDirs": [], /* 指定一个路径列表,在构建时编译器会将这个路径列表中的路径的内容都放到一个文件夹中 */
// "typeRoots": [], /* 指定声明文件或文件夹的路径列表,如果指定了此项,则只有在这里列出的声明文件才会被加载 */
// "types": [], /* 指定需要包含的模块,只有在这里列出的模块的声明文件才会被加载进来 */
// "allowSyntheticDefaultImports": true, /* 允许引入没有默认导出的模块 */
"esModuleInterop": true /* 通过为导入内容创建命名空间,实现CommonJS和ES模块之间的互操作性。需要配置 'allowSyntheticDefaultImports'. */,
// "preserveSymlinks": true, /* 不把符号链接解析为其真实路径 */
// "allowUmdGlobalAccess": true, /* 允许从模块访问 UMD 全局变量 */

/*
* Source Map Options
*/
// "sourceRoot": "", /* 指定调试器应该找到 TypeScript 文件而不是源文件位置 */
// "mapRoot": "", /* 指定调试器找到映射文件而非生成文件的位置 */
// "inlineSourceMap": true, /* 指定是否将 map 文件的内容和 js 文件编译在同一个 js 文件中 */
// "inlineSources": true, /* 用于指定是否进一步将 *.ts 文件的内容也包含到输入文件中; 需要设置 '--inlineSourceMap' 或者 '--sourceMap' */

/*
* Experimental Options
*/
// "experimentalDecorators": true, /* 指定是否启用实验性的装饰器特性 */
// "emitDecoratorMetadata": true, /* 指定是否为装饰器提供元数据支持 */

/*
* Advanced Options
*/
"forceConsistentCasingInFileNames": true /* 不允许不同变量来代表同一文件 */
}
}

### 基础语法

数组

数组有两张写法,个人推荐第一种,一目了然。

1
2
3
4
5
// 写法一
let list: number[] = [1, 2, 3];

// 写法二
let list: Array<number> = [1, 2, 3];

元组

元组多用于记录确定数量和类型的数组。

1
2
3
let x: [string, number];
x = ['hello', 10]; // OK
x = [10, 'hello']; // Error

当访问一个越界的元素,会使用联合类型替代:

1
2
3
4
x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型
x[6] = true; // Error, 布尔不是(string | number)类型

console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toString

枚举

使用枚举可以定义一些有名字的数字常量

1
2
3
4
5
6
enum Color {
Red,
Green,
Blue,
}
let c: Color = Color.Green;

Any

处理不确定的内容:比如没有 ts 声明的第三方库/用户自定义库

1
2
3
let notSure: any = 4;
notSure = 'maybe a string instead';
notSure = false; // okay, definitely a boolean

注:能不用就尽量不用,因为使用此和不用 ts 没什么区别

Void

表示没有任何类型,常用于无返回值函数

1
2
3
function warnUser(): void {
console.log('This is my warning message');
}

注:声明一个 void 类型的变量没有什么大用,因为你只能为它赋予 undefinednull

Never

never 类型表示的是那些永不存在的值的类型。一般用于报错函数或者无终止条件的函数。

never 类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是 never 的子类型或可以赋值给 never 类型(除了 never 本身之外)。 即使 any 也不可以赋值给 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) {}
}

Object

object 表示非原始类型,也就是除 numberstringbooleansymbolnullundefined 之外的类型。

类型断言

类型断言有两种形式。 其一是“尖括号”语法:

1
2
3
let someValue: any = 'this is a string';

let strLength: number = (<string>someValue).length;

另一个为 as 语法:

1
2
3
let someValue: any = 'this is a string';

let strLength: number = (someValue as string).length;

当在 TypeScript 里使用 JSX 时,只有 as 语法断言是被允许的。

注:类型断言会影响 ts 的类型校验,对于十分确定的情况可以使用断言来减少一些转换问题,但不要滥用。

接口

基本写法

1
2
3
4
5
6
7
8
9
10
11
interface LabelledValue {
label: string;
size?: number;
}

function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}

let myObj = { size: 10, label: 'Size 10 Object' };
printLabel(myObj);

注意:可选属性如果出现报红提示时,需要考虑传入的变量类型为 undefined 的情况,因为 undefined 和 null 是所有基础类型的子集

只读属性

一些对象属性只能在对象刚刚创建的时候修改其值。你可以在属性名前用 readonly 来指定只读属性:

1
2
3
4
interface Point {
readonly x: number;
readonly y: number;
}

TypeScript 具有 ReadonlyArray<T> 类型,它与 Array<T> 相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改。

1
2
3
4
5
6
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!

⚠️:上面代码的最后一行,可以看到就算把整个 ReadonlyArray 赋值到一个普通数组也是不可以的。 但是你可以用类型断言重写:

1
a = ro as number[];

最简单判断该用 readonly 还是 const 的方法是看要把它做为变量使用还是做为一个属性。做为变量使用的话用 const,若做为属性则使用 readonly

额外的属性检查

对象字面量会被特殊对待而且会经过额外属性检查,当将它们赋值给变量或作为参数传递的时候。如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误。

1
2
3
4
5
6
7
8
9
10
11
interface SquareConfig {
color?: string;
width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
// ...
}

// error: 'colour' not expected in type 'SquareConfig'
let mySquare = createSquare({ colour: 'red', width: 100 });

绕开这些检查非常简单。 最简便的方法是使用类型断言:

1
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);

然而,最佳的方式是能够添加一个字符串索引签名,前提是你能够确定这个对象可能具有某些做为特殊用途使用的额外属性。

1
2
3
4
5
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}

⚠️ 注意

还有最后一种跳过这些检查的方式,这可能会让你感到惊讶,它就是将这个对象赋值给一个另一个变量:因为 squareOptions 不会经过额外属性检查,所以编译器不会报错。

1
2
let squareOptions = { colour: 'red', width: 100 };
let mySquare = createSquare(squareOptions);

函数类型

为了使用接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。

1
2
3
4
5
6
7
8
9
interface SearchFunc {
(source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function (source: string, subString: string) {
let result = source.search(subString);
return result > -1;
};

对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配,函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。比如,我们使用下面的代码重写上面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 不对应变量名
let mySearch: SearchFunc;
mySearch = function (src: string, sub: string): boolean {
let result = src.search(sub);
return result > -1;
};

// 什么都不加
let mySearch: SearchFunc;
mySearch = function (src, sub) {
let result = src.search(sub);
return result > -1;
};

可索引的类型

使用接口的方式来为数组进行类型声明

1
2
3
4
5
6
7
8
interface StringArray {
[index: number]: string;
}

let myArray: StringArray;
myArray = ['Bob', 'Fred'];

let myStr: string = myArray[0];

字符串索引签名能够很好的描述 dictionary 模式,并且它们也会确保所有属性与其返回值类型相匹配。 因为字符串索引声明了 obj.propertyobj["property"] 两种形式都可以。下面的例子里,name 的类型与字符串索引类型不匹配,所以类型检查器给出一个错误提示:

1
2
3
4
5
interface NumberDictionary {
[index: string]: number;
length: number; // 可以,length是number类型
name: string; // 错误,`name`的类型与索引类型返回值的类型不匹配
}

函数

剩余参数

剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。

1
2
3
4
5
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + ' ' + restOfName.join(' ');
}

let employeeName = buildName('Joseph', 'Samuel', 'Lucas', 'MacKinzie');

声明文件

UMD

UMD 模块是指那些既可以作为模块使用(通过导入)又可以作为全局(在没有模块加载器的环境里)使用的模块。 许多流行的库,比如 Moment.js,就是这样的形式。 比如,在 Node.js 或 RequireJs 里,你可以这样写:

1
2
import moment = require('moment');
console.log(moment.format());

namespace 和 module

namespace

TS 里的 namespace 主要是解决命名冲突的问题,会在全局生成一个对象,定义在 namespace 内部的类都要通过这个对象的属性访问。对于内部模块来说,尽量使用 namespace 替代 module,可参考官方文档。例如:

1
2
3
4
5
6
7
8
9
10
11
12
namespace Test {
export const USER_NAME = 'test name';

export namespace Polygons {
export class Triangle {}
export class Square {}
}
}

// 取别名
import polygons = Test.Polygons;
const username = Test.username;

注意:import xx = require(‘xx’) 为加载模块的写法,不要与取别名的写法混淆。

默认全局环境的 namespace 为 global

module

模块可理解成 Vue 中的单个 vue 文件,它是以功能为单位进行划分的,一个模块负责一个功能。其与 namespace 的最大区别在于:namespace 是跨文件的,module 是以文件为单位的,一个文件对应一个 module。类比 Java,namespace 就好比 Java 中的包,而 module 则相当于文件。

参考文档