春节假期延长,在家学习 TypeScript.
本篇主要讲三部分内容:命名空间、声明合并、编译工具. 如果您想要得到最新的更新,可以点击下面的链接:
命名空间
在 JavaScript 中,命名空间能有效的避免全局污染。在 es6
引入了模块系统之后,命名空间就很少被使用了。但 TS 中依然实现了这个特性,尽管在模块系统中,我们不必考虑全局污染情况,但如果使用了全局的类库,命名空间仍然是一个比较好的解决方案。
例子
首先创建两个 ts 文件,分别为 a.ts
和 b.ts
代码如下:
本篇大部分为代码段,可以点击查看源码运行阅读。
// ./src/a.ts
namespace Shape {
const pi = Math.PI
export function cricle (r: number) {
return pi * r ** 2
}
}
如果让成员在全局可见,需要使用 export
关键字输出。
// ./src/b.ts
namespace Shape {
export function square (x: number) {
return x * x
}
}
console.log(Shape.cricle(1))
console.log(Shape.square(1))
同样在 b.ts
文件中也声明了命名空间,在底部调用了 Shape.cricle
和 Shape.square
两个方法。
这时我们进行编译的话,因为 cricle
属性存在于 a.ts
文件内,会报出 error TS2339: Property 'cricle' does not exist on type 'typeof Shape'.
错误,这里我们可以使用三斜线指令
三斜线指令
三斜线指令是包含单个XML标签的单行注释。 注释的内容会做为编译器指令使用。
三斜线指令仅可放在包含它的文件的最顶端。 一个三斜线指令的前面只能出现单行或多行注释,这包括其它的三斜线指令。 如果它们出现在一个语句或声明之后,那么它们会被当做普通的单行注释,并且不具有特殊的涵义。
/// <reference path="..." />
指令是三斜线指令中最常见的一种。 它用于声明文件间的 依赖。
三斜线引用告诉编译器在编译过程中要引入的额外的文件。
/// <reference path="a.ts" />
namespace Shape {
export function square (x: number) {
return x * x
}
}
console.log(Shape.cricle(1))
console.log(Shape.square(1))
我们在执行一下打包命令 tsc ./src/b.ts
会生成 a.js
和 b.js
两个文件。
// ./src/a.js
var Shape;
(function (Shape) {
var pi = Math.PI;
function cricle(r) {
return pi * Math.pow(r, 2);
}
Shape.cricle = cricle;
})(Shape || (Shape = {}));
// ./src/b.js
/// <reference path="a.ts" />
var Shape;
(function (Shape) {
function square(x) {
return x * x;
}
Shape.square = square;
})(Shape || (Shape = {}));
console.log(Shape.cricle(1));
console.log(Shape.square(1));
命名空间在编译之后其实就是一个闭包。
为了实现全局引用效果,我们用 script
标签引入两个文件。
console.log(Shape.cricle(1)) // 3.141592653589793
console.log(Shape.square(1)) // 1
为了调用方便,我们可以为方法设置别名。
// 别名
import cricle = Shape.cricle
console.log(cricle(2)) // 12.566370614359172
注意这里的 import
关键字并不是 es6
中的 import
。
声明合并
编译器会把程序多个地方具有相同名称的声明合并为一个声明,合并后的声明同时拥有原先两个声明的特性。
接口声明合并
interface A {
x: number
}
interface A {
y: number
}
let a: A = {
x: 1,
y: 2
}
上述例子中,我们定义了两个同名接口 A,各自添加了一个属性。我们在定义一个变量 a
,它的类型设置为接口 A
,这时 a
就要具备两个同名接口的属性。
如果两个同名函数中具有相同的属性时会怎样呢?如下所示:
interface A {
x: number
}
interface A {
x: number
}
interface B {
y: string
}
interface B {
y: number
}
// Error: Subsequent property declarations must have the same type.Property 'y' must be of type 'string', but here has type 'number'.
同名接口中,成员属性相同时类型必须相同。
若成员属性为函数时,每个函数都会被声明为一个函数重载。
interface A {
foo (bar: number): number // 3
}
interface A {
foo (bar: string): string // 1
foo (bar: string[]): string[] // 2
}
let a: A = {
foo (bar: any) {
return bar
}
}
每组接口里的声明顺序保持不变,但各组接口之间的顺序是后来的接口重载出现在靠前位置。上述函数重载列表优先级顺位为注释所示。
如果签名里有一个参数的类型是单一的字符串字面量,那么它将会被提升到重载列表的最顶端。
命名空间与类、函数、枚举类型合并
命名空间可以与其它类型的声明进行合并。 只要命名空间的定义符合将要合并类型的定义。合并结果包含两者的声明类型。
命名空间与类
class C {}
namespace C {
export let version = '1.0.0'
}
console.log(C.version) // 1.0.0
命名空间与函数
function Lib () {}
namespace Lib {
export let version = '1.0.0'
}
console.log(Lib.version) // 1.0.0
相当于为函数添加了一些默认属性。
命名空间与枚举
enum Color {
Red,
Blue
}
namespace Color {
export let version = '1.0.0'
}
console.log(Color.version) // 1.0.0
命名空间与类、命名空间与函数合并时,命名空间要在其之后。
编译工具
本篇主要介绍 ts-loader、awesome-typescript-loader 和 babel 分别编译 Typescript 的区别
ts-loader
ts-loader 内部调用了 tsc
,所以在使用 ts-loader
时,会使用 tsconfig.json
配置文件。
提高构建速度
当项目中的代码变的越来越多,体积也越来越庞大时,项目编译时间也随之增加。这是因为 Typescript 的语义检查器必须在每次重建时检查所有文件。 ts-loader
提供了一个 transpileOnly
选项,它默认为 false
,我们可以把它设置为 true
,这样项目编译时就不会进行类型检查,也不会输出声明文件。
module.exports = {
...
module: {
rules: [
{
test: /\.tsx?$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true
}
}
]
}
]
}
}
我们对一下 transpileOnly
分别设置 false
或 true
的项目构建速度。如下:
$ yarn build
yarn run v1.12.3
$ webpack --mode=production --config ./build/webpack.config.js
Hash: 36308e3786425ccd2e9d
Version: webpack 4.41.0
Time: 2482ms
Built at: 12/20/2019 4:52:43 PM
Asset Size Chunks Chunk Names
app.js 932 bytes 0 [emitted] main
index.html 338 bytes [emitted]
Entrypoint main = app.js
[0] ./src/index.ts 14 bytes {0} [built]
Child html-webpack-plugin for "index.html":
1 asset
Entrypoint undefined = index.html
[0] ./node_modules/html-webpack-plugin/lib/loader.js!./index.html 489 bytes {0} [built]
[2] (webpack)/buildin/global.js 472 bytes {0} [built]
[3] (webpack)/buildin/module.js 497 bytes {0} [built]
+ 1 hidden module
✨ Done in 4.88s.
$ yarn build
yarn run v1.12.3
$ webpack --mode=production --config ./build/webpack.config.js
Hash: e5a133a9510259e1f027
Version: webpack 4.41.0
Time: 726ms
Built at: 12/20/2019 4:54:20 PM
Asset Size Chunks Chunk Names
app.js 932 bytes 0 [emitted] main
index.html 338 bytes [emitted]
Entrypoint main = app.js
[0] ./src/index.ts 14 bytes {0} [built]
Child html-webpack-plugin for "index.html":
1 asset
Entrypoint undefined = index.html
[0] ./node_modules/html-webpack-plugin/lib/loader.js!./index.html 489 bytes {0} [built]
[2] (webpack)/buildin/global.js 472 bytes {0} [built]
[3] (webpack)/buildin/module.js 497 bytes {0} [built]
+ 1 hidden module
✨ Done in 2.40s.
当 transpileOnly
为 false 时,整体构建时间为 4.88s
,当 transpileOnly
为 true 时,整体构建时间为 2.40s
虽然构建速度提升了,但是有了一个弊端打包编译不会进行类型检查。
fork-ts-checker-webpack-plugin
这里官方推荐了一个解决方案,使用 fork-ts-checker-webpack-plugin,它在一个单独的进程上运行类型检查器,该插件在编译之间重用抽象语法树,并与TSLint共享这些树。可以通过多进程模式进行扩展,以利用最大的CPU能力。
需要注意的是,此插件使用 TypeScript 而不是 webpack 的模块解析,这一点非常重要。这意味着你必须正确设置 tsconfig.json。例如,如果您在 tsconfig.json 中设置文件:['./src/someFile.ts'],则此插件将仅检查 someFile.ts 的语义错误。这是为了构建性能。该插件的目标是尽可能快。有了 TypeScript 的模块解析,我们不必等待 webpack 编译文件(在编译过程中会遍历依赖图)-我们从一开始就拥有完整的文件列表。
要调试 TypeScript 的模块解析,可以使用 tsc --traceResolution
命令。
使用 fork-ts-checker-webpack-plugin
安装
$ yarn add -D fork-ts-checker-webpack-plugin
webpack.config.js 中修改:
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
module.exports = {
...
module: {
rules: [
{
test: /\.tsx?$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true
}
}
]
}
]
},
plugins: [
new ForkTsCheckerWebpackPlugin()
]
}
这样在构建的时候,既保证了构建速度,又会对其做类型检查。
它提供了更多的配置,如:reportFiles
new ForkTsCheckerWebpackPlugin({
reportFiles: ['src/**/*.{ts,tsx}']
})
这里表示只报告与这些全局模式匹配的文件的错误。更多选项可以自行查阅。
awesome-typescript-loader
与 ts-loader 对比
- atl 更适合于 Babel 集成,当启用了 useBabel 和 useCache 标志时,typescript 的派发将被 Babel 替换并缓存,下次源文件和环境有相同的校验,我们可以完全跳过 typescript 和 Babel 的转换。
- atl 能够将类型检查器和发射器发送到一个单独的进程,这也加快了类似热更新的开发场景。
- 不需要安装额外的插件。
使用
$ yarn add awesome-typescript-loader
webpack.config.js
rules: [
{
test: /\.tsx?$/i,
use: [{
loader: 'awesome-typescript-loader',
options: {
transpileOnly: true
}
}],
exclude: /node_modules/
}
]
atl 本身提供了检查插件 CheckerPlugin
,检查 ts 语法错误。
const { CheckerPlugin } = require('awesome-typescript-loader')
...
plugins: [
new CheckerPlugin()
]
当
transpileOnly
设置为 true 时,CheckerPlugin
将会无效
babel 编译
TSC 与 Babel 对比
- | 编译能力 | 类型检查 | 插件 |
---|---|---|---|
TSC | ts(x)、js(x) -> es3/5/6/... | 有 | 无 |
Babel | ts(x)、js(x) -> es3/5/6/... | 无 | 丰富 |
Babel 没有类型检查机制,可以配合 typescript tsc --watch
.
安装使用
安装包
"@babel/cli": "^7.4.4",
"@babel/core": "^7.4.5",
"@babel/plugin-proposal-class-properties": "^7.4.4",
"@babel/plugin-proposal-object-rest-spread": "^7.4.4",
"@babel/preset-env": "^7.4.5",
"@babel/preset-typescript": "^7.3.3"
plugin-proposal-class-properties
支持 class、plugin-proposal-object-rest-spread
支持对象解构赋值
.babelrc
{
"presets": [
"@babel/env",
"@babel/preset-typescript"
],
"plugins": [
"@babel/proposal-class-properties",
"@babel/proposal-object-rest-spread"
]
}
4种书写方式 Babel 无法编译
- 命名空间
namespace N {
export const n = 1
}
- 类型断言只允许使用
as
let s = <A>{}
- 常量枚举
const enum E { A }
- 默认导出
export = s
总结
- 如果没有使用过 Babel,应使用 TypeScript 自身编译器,可配合 ts-loader.
- 如果项目中已经安装使用了 Babel,可以安装 @babel/preset-typescript,配合 tsc 做类型检查.
- 两种编译工具不要混合使用.