编译
编译时将包含哪些 ts 文件
files
、include
选项指定的文件,都会被加入到编译过程中- 默认情况下,TypeScript 将导入
node_modules/@types
目录下所有包的类型声明文件,即使有些包并没有import
到我们的源码里。 import
语句涉及到的包
TypeScript 在将.ts
文件编译为.js
文件时,只会针对语法层面做转换,而不会添加polyfills
。比如当将target
设置为ES5
时,会将let
转换成var
,也会将箭头函数转成常规的函数表达式,但是不会处理Promise
,Promise
会保留在最终的产出文件里。因此需要我们在运行时自己添加polyfills
。
重难点说明
类型去除
TypeScript 在编译时会将类型声明及类型声明的import
都移除掉,因为类型声明并不会打包到产出文件里。
若是通过import
引入的成员同时可以是类型和值,需要判断这个成员是否作为“值”被使用了,若是,则编译时需要保留import
该成员,否则移除import
语句。
// person.ts
export class Person {
name: string;
}
2
3
4
// index.ts
import { Person } from './person';
const person: Person = {
name: 'wind-stone'
}
2
3
4
5
6
Person
既是值也是类型,若上的代码里,Person
仅是被作为类型使用,因此编译结果是:
// index.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const person = {
name: 'wind-stone'
};
2
3
4
5
6
可以发现,产出文件里并没有import
语句对应的代码。
若是将Person
作为类使用:
// index.ts
import { Person } from './person';
const person: Person = {
name: 'wind-stone'
}
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');
2
3
4
5
在声明文件里有声明但在运行时里不一定有对应实现
// my-file.ts
// append global `Number` interface
interface Number {
isEven(): boolean;
}
const num = 10;
num.isEven()
2
3
4
5
6
7
8
9
10
上面的代码通过对全局Number
进行扩展,添加了isEven
方法,因此num.isEven()
不会引起 TypeScript 的报错。
但是上面的源码经过 TypeScript 编译后,变成:
"use strict";
const num = 10;
// 上面通过对全局 Number 进行扩展
num.isEven();
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
2
3
4
5
6
我们先来看一下box
目录下的各个文件。
// box/index.js
"use strict";
exports.__esModule = true;
exports["default"] = 'hello';
2
3
4
// box/index.d.ts
declare const _default: "box";
export default _default;
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(静态分析)
}
2
3
4
5
6
7
之后,我们在program.ts
里导入box
模块。
// program.ts
import box from './box';
console.log(box)
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"]);
2
3
4
5
我们发现,最终产出的program.js
文件里是通过require("./box")
来导入box
模块的,且模块的路径仍为./box
。
node program.js
# 输出: hello
2
3
只是当我们通过node program.js
执行编译后的文件,当遇到require('./box')
语句时,Node.js 的模块解析系统会找到box/index.js
文件进行执行并获得执行结果,因此最终的输出的结果是hello
。
这也说明,TypeScript 编译器在导入模块时,只导入该模块的.ts
或.d.ts
文件(而不关心该模块的main
或module
指向的文件是怎么实现该模块的),并在编译时将原先的import
语句转换成对应模块系统的导入语句(比如针对 CommonJS 来说,变成了require
)并保持模块路径不变。
在最终执行编译后的文件时,再根据执行环境对应的模块系统的解析规则去解析出该模块的入口文件具体是哪个。针对 CommonJS 来说,会使用main
指向的文件;针对 ES Module 来说,会使用module
指向的文件。
为什么 TypeScript 代码里的 import 语句即可以引入值也可以引入类型声明?
import axios, { Method } from 'axios';
以上面的代码为例,引入的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; // 新增的常量声明
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);
2
3
4
由此我们可以发现,实际上 TypeScript 在做静态分析时,是通过import
语句去查找模块的类型声明文件.d.ts
,并检查导入进来的这些值或类型声明是否存在于类型声明文件里,若不存在则报错提示,否则导入正常。
在 TypeScript 做静态分析时,完全不涉及到运行时,若导入的值已在模块的类型声明文件里声明过,则 TypeScript 就认为该值在运行时也会存在。