一起学 TypeScript 进阶篇

4,749 阅读6分钟

春节假期延长,在家学习 TypeScript.

本篇主要讲三部分内容:命名空间声明合并编译工具. 如果您想要得到最新的更新,可以点击下面的链接:

TypeScript开发教程 文档版

TypeScript开发教程 GitHub

命名空间

在 JavaScript 中,命名空间能有效的避免全局污染。在 es6 引入了模块系统之后,命名空间就很少被使用了。但 TS 中依然实现了这个特性,尽管在模块系统中,我们不必考虑全局污染情况,但如果使用了全局的类库,命名空间仍然是一个比较好的解决方案。

例子

首先创建两个 ts 文件,分别为 a.tsb.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.cricleShape.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.jsb.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 分别设置 falsetrue 的项目构建速度。如下:

$ 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 对比

  1. atl 更适合于 Babel 集成,当启用了 useBabel 和 useCache 标志时,typescript 的派发将被 Babel 替换并缓存,下次源文件和环境有相同的校验,我们可以完全跳过 typescript 和 Babel 的转换。
  2. atl 能够将类型检查器和发射器发送到一个单独的进程,这也加快了类似热更新的开发场景。
  3. 不需要安装额外的插件。

使用

$ 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 无法编译

  1. 命名空间
namespace N {
  export const n = 1
}
  1. 类型断言只允许使用 as
let s = <A>{}
  1. 常量枚举
const enum E { A }
  1. 默认导出
export = s

总结

  1. 如果没有使用过 Babel,应使用 TypeScript 自身编译器,可配合 ts-loader.
  2. 如果项目中已经安装使用了 Babel,可以安装 @babel/preset-typescript,配合 tsc 做类型检查.
  3. 两种编译工具不要混合使用.

链接

ts-loader、awesome-typescript-loader 示例

typescript-babel 示例

回顾基础篇