编译

编译时将包含哪些 ts 文件

  • filesinclude选项指定的文件,都会被加入到编译过程中
  • 默认情况下,TypeScript 将导入node_modules/@types目录下所有包的类型声明文件,即使有些包并没有import到我们的源码里。
  • import语句涉及到的包

TypeScript 在将.ts文件编译为.js文件时,只会针对语法层面做转换,而不会添加polyfills。比如当将target设置为ES5时,会将let转换成var,也会将箭头函数转成常规的函数表达式,但是不会处理PromisePromise会保留在最终的产出文件里。因此需要我们在运行时自己添加polyfills

重难点说明

类型去除

TypeScript 在编译时会将类型声明及类型声明的import都移除掉,因为类型声明并不会打包到产出文件里。

若是通过import引入的成员同时可以是类型和值,需要判断这个成员是否作为“值”被使用了,若是,则编译时需要保留import该成员,否则移除import语句。

// person.ts
export class Person {
  name: string;
}
1
2
3
4
// index.ts
import { Person } from './person';

const person: Person = {
    name: 'wind-stone'
}
1
2
3
4
5
6

Person既是值也是类型,若上的代码里,Person仅是被作为类型使用,因此编译结果是:

// index.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const person = {
    name: 'wind-stone'
};
1
2
3
4
5
6

可以发现,产出文件里并没有import语句对应的代码。

若是将Person作为类使用:

// index.ts
import { Person } from './person';

const person: Person = {
    name: 'wind-stone'
}
1
2
3
4
5
6

则产出文件里会保留import语句,并将其编译为对应的模块导入语句。

// index.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const person_1 = require("./person");
const person = new person_1.Person('wind-stone');
1
2
3
4
5

在声明文件里有声明但在运行时里不一定有对应实现

// my-file.ts

// append global `Number` interface
interface Number {
  isEven(): boolean;
}

const num = 10;

num.isEven()
1
2
3
4
5
6
7
8
9
10

上面的代码通过对全局Number进行扩展,添加了isEven方法,因此num.isEven()不会引起 TypeScript 的报错。

但是上面的源码经过 TypeScript 编译后,变成:

"use strict";
const num = 10;
// 上面通过对全局 Number 进行扩展
num.isEven();
1
2
3
4

在实际运行时,因为并没有实现Number上的isEven方法,会导致运行时报错。

为什么 TypeScript 文件里的 import 即可以导入模块的实现也能导入模块的类型声明文件?

TypeScript 编译器在处理import模块时,只会import这个模块的.ts文件或.d.ts(类型声明文件),而不会导入这个模块具体的实现文件。当经过 TypeScript 编译后的文件被执行时,才会导入模块具体的实现文件。

假设项目结构是这样的:

project
├── program.ts
└── box/
   ├── index.d.ts
   ├── index.js
   └── package.json
1
2
3
4
5
6

我们先来看一下box目录下的各个文件。

// box/index.js
"use strict";
exports.__esModule = true;
exports["default"] = 'hello';
1
2
3
4
// box/index.d.ts
declare const _default: "box";
export default _default;
1
2
3

这里需要说明的是,尽管我们在box模块的类型声明文件里声明了该模块会存在默认导出,导出的值是字符串类型的box,但是在box/index.js里我们实现的是默认导出hello

// box/package.json
{
    "name": "box",
    "version": "1.0.0",
    "main": "index.js", // 供 Node.js 导入,以执行该文件并返回结果(运行时)
    "typings": "index.d.ts" // 供 TypeScript 编译器或 IDE 导入,以了解该模块的 API(静态分析)
}
1
2
3
4
5
6
7

之后,我们在program.ts里导入box模块。

// program.ts
import box from './box';

console.log(box)
1
2
3
4

最后,我们执行tsc program.ts得到program.js文件。

// program.js
"use strict";
exports.__esModule = true;
var box_1 = require("./box");
console.log(box_1["default"]);
1
2
3
4
5

我们发现,最终产出的program.js文件里是通过require("./box")来导入box模块的,且模块的路径仍为./box

node program.js

# 输出: hello
1
2
3

只是当我们通过node program.js执行编译后的文件,当遇到require('./box')语句时,Node.js 的模块解析系统会找到box/index.js文件进行执行并获得执行结果,因此最终的输出的结果是hello

这也说明,TypeScript 编译器在导入模块时,只导入该模块的.ts.d.ts文件(而不关心该模块的mainmodule指向的文件是怎么实现该模块的),并在编译时将原先的import语句转换成对应模块系统的导入语句(比如针对 CommonJS 来说,变成了require)并保持模块路径不变。

在最终执行编译后的文件时,再根据执行环境对应的模块系统的解析规则去解析出该模块的入口文件具体是哪个。针对 CommonJS 来说,会使用main指向的文件;针对 ES Module 来说,会使用module指向的文件。

为什么 TypeScript 代码里的 import 语句即可以引入值也可以引入类型声明?

import axios, { Method } from 'axios';
1

以上面的代码为例,引入的axios是个值,引入的Method是个枚举类型的类型声明。

我们打开axios的类型声明文件node_modules/axios/index.d.ts

// ...
export type Method =
  | 'get' | 'GET'
  | 'delete' | 'DELETE'
  | 'head' | 'HEAD'
  | 'options' | 'OPTIONS'
  | 'post' | 'POST'
  | 'put' | 'PUT'
  | 'patch' | 'PATCH'
  | 'purge' | 'PURGE'
  | 'link' | 'LINK'
  | 'unlink' | 'UNLINK'

// ...

export interface AxiosStatic extends AxiosInstance {
  create(config?: AxiosRequestConfig): AxiosInstance;
  Cancel: CancelStatic;
  CancelToken: CancelTokenStatic;
  isCancel(value: any): boolean;
  all<T>(values: (T | Promise<T>)[]): Promise<T[]>;
  spread<T, R>(callback: (...args: T[]) => R): (array: T[]) => R;
}

declare const Axios: AxiosStatic;

export default Axios;


export const helloWorld; // 新增的常量声明
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

从类型声明文件可知,默认导出的Axios(项目里引入时命名为axios)已经声明为一个常量,其类型为AxiosStatic,而导出的Method是个类型声明。

在静态分析时,import语句能引入什么内容,值或是类型,完全取决于该模块的类型声明文件里会导出哪些内容,这与运行时是否存在导入的这些内容无关。

举个例子,我们在axios的类型声明文件node_modules/axios/index.d.ts里新增一条声明export const helloWorld,而这个常量helloWorld并没有在运行时实现。但是我们可以在代码里import进来并打印出来,在 TypeScript 做静态分析时这并不会报错,但是在运行时一定会报错。

// 额外引入 helloWorld 并打印
import axios, { Method, helloWorld } from 'axios';

console.log(helloWorld);
1
2
3
4

由此我们可以发现,实际上 TypeScript 在做静态分析时,是通过import语句去查找模块的类型声明文件.d.ts,并检查导入进来的这些值或类型声明是否存在于类型声明文件里,若不存在则报错提示,否则导入正常。

在 TypeScript 做静态分析时,完全不涉及到运行时,若导入的值已在模块的类型声明文件里声明过,则 TypeScript 就认为该值在运行时也会存在。