0%

JavaScript 权威指南 17:JavaScript 工具和扩展

接下来会介绍几种重要的编程工具,很多 JavaScript 程序员都经常使用。此外还会介绍对核心 JavaScript 语言的两个使用很广泛的扩展。

使用 ESLint 检查代码

在编程领域,lint 是指代码虽然技术上正确,但书写却不够规范,甚至可能有 bug,或者没有达到最优。linter 是用于检查代码中 lint 的工具。目前最常用的 JavaScript linter 是 ESLint。如果运行它并花时间实际解决它指出的问题,你的代码会更清晰,更不容易出错。

ESLint 定义了很多 linting 规则,而且有一个插件生态,可以增加新规则。但 ESLint 也是完全可以配置的,可以定义一个配置文件,让 ESLint 只执行你想让它执行的规则。

使用 Prettier 格式化代码

Prettier 工具可以用来自动解析和重新格式化代码。

  • 如果调用 Prettier 时带了 --write 选项,它只会重新格式化指定的文件,而不会把结果打印出来
  • 如果使用 git 管理源代码,可以通过提交钩子(hook)以 --write 选项调用 Prettier,从而让代码在检入前自动格式化

使用 Jest 做单元测试

JavaScript 这样的动态语言支持测试框架,可以大幅减少编写测试的工作量,甚至能让写测试变得很好玩。JavaScript 有很多测试工具和库,很多是以模块化方式编写的。Jest 是一个囊括所有测试功能的流行框架。

使用 npm 管理依赖包

在现代软件开发中,稍微复杂点的程序都会依赖一些第三方软件库。包管理工具就可以让发现和安装第三方软件包更方便。同样重要的是,包管理工具可以跟踪你的代码依赖哪些包,并把这些信息保存到一个文件中。这样当别人也想尝试运行你的程序时,他们可以下载你的代码以及你的依赖列表,然后使用自己的包管理工具安装你的代码需要的所有第三方包。

npm 是 Node.js 的包管理工具,也是 JavaScript 社区中最流行的包管理器:

  • 如果要尝试别人写的 JavaScript 项目,在下载其代码后,通常第一件事就是键入 npm install。这个命令会读取 package.json 文件中的依赖列表,并下载项目依赖的第三方包并保存到 node_modules 目录中
  • 使用 npm install <package-name> 在项目的 node_modules 目录安装特定的包,例如:
1
npm install express
  • 除了使用包名来安装依赖之外,npm 也会在项目的 package.json 文件中添加一条记录。这样把依赖记录下来可以让别人只键入 npm install 就可以安装全部依赖

还有一种依赖只对项目的开发者有用,项目运行的时候并不需要。比如,项目中使用 Prettier 来保证代码格式统一,但 Prettier 属于 开发依赖​,安装它的时候可以添加 --save-dev

1
npm install --save-dev prettier
  • 还可能需要全局安装某个开发者工具,从而即便在没有 package.json 文件和 node_modules 目录的地方也可以使用。为此可以在安装依赖时添加 -g(即 global,全局)选项:
1
2
3
4
5
6
# npm install -g eslint jest

# which eslint
/usr/local/bin/eslint
# which jest
/usr/local/bin/jest
  • 除了 install 命令之外,npm 还支持 uninstall 和 update 命令,用于删除和更新依赖。而 audit 命令可以找到并修复依赖中的安全漏洞

在项目中本地安装 ESLint 等工具时,eslint 脚本会保存在 ./node_modules/.bin/eslint 目录中,因此运行命令比较麻烦。**好在 npm 也附带了一个 npx 命令,这样就可以用 npx eslintnpx jest 命令来运行本地安装的工具:

1
2
ls -l ./node_modules/.bin/eslint
lrwxrwxrwx 1 root root 23 Feb 4 10:14 ./node_modules/.bin/eslint -> ../eslint/bin/eslint.js
1
2
3
4
5
6
# npx eslint t.js
......

1:5 error 't' is assigned a value but never used no-unused-vars

1 problem (1 error, 0 warnings)

代码打包

如果要写一个在浏览器中运行的大型 JavaScript 项目,可能会用到代码打包工具,特别是在使用的外部库是以模块形式提供的时候。Web 开发者已经使用 ES6 模块很多年了,最初浏览器尚未支持 import 和 export 关键字:

  • 为了使用 ES6 模块,程序员使用代码打包工具从程序的主入口(或多入口)开始,跟着 import 指令树,从而找到程序依赖的所有模块
  • 然后把所有独立的模块文件组合成一个 JavaScript 代码包,并重写 import 和 export 指令让代码可以在这种新形式下运行
  • 结果是一个代码文件,让不支持模块的浏览器可以加载运行

ES6 模块今天已经得到浏览器的普遍支持,但 Web 开发者仍然倾向于使用代码打包工具,至少在发布产品代码时要使用。开发者发现在用户首次访问网站时,相比于加载多个小型模块,加载一个中等大小的代码包时用户体验最佳。

市面上有很多优秀的 JavaScript 打包工具可供选择。其中常用的有 webpack、Rollup 和 Parcel。除了基本的打包功能之外,打包工具也会提供其他一些特性:

  • 有些程序可能有多个入口。比如,多页 Web 应用可以为每个页面都写一个入口。打包工具通常支持为每个入口创建一个代码包,或者生成一个支持多入口的独立包
  • 函数可以以函数式而非静态形式使用 import(),避免在程序启动时加载所有代码,而是在需要时再动态加载
  • 打包工具通常会输出源码映射(source map)文件,包含源代码行号与输出包代码行号的映射。这样可以方便浏览器开发者工具自动显示 JavaScript 错误
  • 有时候在向程序中导入一个模块时,实际上只会用到其中少量特性。优秀的打包工具会分析代码,找出未使用的部分并在打包时排除它们,这个特性也被称为 摇树优化
  • 打包工具通常有某种基于插件的架构,支持导入和打包非 JavaScript 代码的文件
  • 打包工具通常支持文件系统监控,可以检测项目目录下文件的修改,自动重新生成必要的代码包。有了这个特性,你可以像往常一样保存代码,然后刷新浏览器就能看到效果
  • 有些打包工具也支持 热模块替换 开发模式,即每次重新生成代码包,都会自动把它们加载到浏览器

使用 Babel 转译

Babel 是一个编译工具,可以把使用现代语言特性编写的 JavaScript 代码编译为不使用那些现代语言特性的 JavaScript 代码。因为是把 JavaScript 代码编译成 JavaScript 代码,Babel 有时候也被称为 转译器(transpiler)。

Babel 的目的就是让开发者可以使用 ES6 及之后的新语言特性,同时仍然可以兼容那些只支持 ES5 的浏览器。一般来说,Babel 输出的代码并没有考虑人类的易读性。不过跟打包工具类似,Babel 也可以生成源码映射,保存转换后的代码与原始代码位置之间的映射,这对使用转换后的代码特别有帮助。

  • 可以使用 npm 安装 Babel,使用 npx 运行它
  • Babel 读取 .babelrc 配置文件,获取你对如何转换 JavaScript 的要求

Babel 提供了一些预设(preset),反映了不同的代码转译激进程度。其中一个比较有意思的 Babel 预设是用于代码压缩的(通过删除注释、空格和重命名变量等)​。

如果同时使用 Babel 和代码打包工具,你应该可以设置打包工具在打包时自动对 JavaScript 文件运行 Babel。这样可以简化产生可运行代码的过程。

今天,虽然转换核心 JavaScript 语言的需求已经变少了,但 Babel 仍然常用于转换对语言的非标准扩展。接下来我们会介绍其中两个这样的语言扩展。

JSX:JavaScript中的标记表达式

JSX 是对核心 JavaScript 的扩展,它使用 HTML 风格的语法定义元素树。JSX 与构建用户界面的 React 框架联系最为紧密。在 React 中,这个使用 JSX 定义的元素树最终会被渲染为 HTML 而进入浏览器。

可以把 JSX 元素想象为一种新的 JavaScript 表达式语法。JavaScript 字符串字面量是以引号来定界的,而正则表达式是以斜杠来定界的。同样,JSX 表达式字面量是以尖括号来定界的。下面是一个简单的 JSX 赋值表达式:

1
let line = <hr/>;

如果你使用 JSX,那么需要使用 Babel(或类似工具)把 JSX 表达式编译为常规 JavaScript。例如 Babel 会把上面赋值语句中的 JSX 表达式转换为下面这个简单的函数调用:

1
let line = React.createElement("hr", null);

JSX 语法类似 HTML,而且与 HTML 元素类似,React 元素也可以像下面这样声明属性:

1
let image = <img src="log.png" alt="The JSX logo" hidden/>;

当元素包含一个或多个属性时,这些属性就会成为对象的属性,作为第二个参数传递给 React.createElement

1
let image = React.createElement("img", {src: "log.png", alt: "The JSX logo", hidden: true});

与 HTML 元素类似,JSX 元素可以将字符串或其他元素作为子元素。JSX 元素也可以任意嵌套,从而创建一棵元素树:

1
2
3
4
5
6
7
let sidebar = (
<div className="sidebar">
<h1>Title</h1>
<hr/>
<p>This is a sidebar content</p>
</div>
)

这些嵌套的 JSX 表达式会转换为一组嵌套的 createElement() 调用。如果 JSX 元素有子元素,那些子元素(字符串或其他JSX元素)会作为第三个及后续参数:

1
2
3
4
5
6
let sidebar = React.createElement(
"div", {className: "sidebar"},
React.createElement("h1", null, "Title"),
React.createElement("hr", null),
React.createElement("p", null, "This is a sidebar content"));
)

React.createElement() 的返回值是一个普通的 JavaScript 对象,React 可以用来渲染在浏览器窗口中的输出。需要注意,可以配置 Babel 把 JSX 元素编译为对一个不同函数的调用,所以 JSX 也可以用于非 React 场景。

JSX 语法的一个重要特性是可以在 JSX 表达式中嵌入常规的 JavaScript 表达式。在 JSX 表达式中,位于花括号内的文本会被当成普通的 JavaScript 来解释。这些嵌套的表达式可以用于生成属性值,也可以用于创建子元素。

1
2
3
4
5
6
7
8
9
function sidebar(className, title, content, drawLine=true) {
return (
<div className={className}>
<h1>{title}</h1>
{drawLine && <hr/>}
<p>{content}</p>
</div>
)
}

既然我们知道 JSX 会编译成函数调用,那么自然也就可以理解它能够包含任意表达式了,因为函数调用也可以写成任意表达式的形式。上述代码经过 Babel 转译后会变成如下样子:

1
2
3
4
5
6
function sidebar(className, title, content, drawLine = true) {
return React.createElement("div", {className: className},
React.createElement("h1", null, title),
drawLine && React.createElement("hr", null),
React.createElement("p", null, content));
}

在 JSX 表达式中使用 JavaScript 表达式时,并不限于前面例子中出现的简单字符串值或布尔值。任何 JavaScript 值都是允许的。事实上,在 React 编程中使用对象、数组和函数都相当常见。例如:

1
2
3
4
5
6
7
8
9
10
11
function list(items, callback) {
return (
<ul style={ {padding:10, border:"solid red 4px"} }>
{
items.map((item, index) => {
<li onClick={() => callback(index)} key={index}>{item}</li>
})
}
</ul>
)
}

JSX中对象表达式的另一种使用场景是使用对象扩展操作符一次性指定多个属性。例如:

1
2
let hebrew = {lang: "he", dir: "rtl"};
let shalm = <span className="emphasis" {...hebrew}>xxx</span>;

Babel 会把这种语法编译为一个 _extends() 函数(这里省略)调用,包含 className 属性和 hebrew 对象中的属性:

1
let shalom = React.createElement("span", _extends({className: "emphasis"}, hebrew), "xxx");

JSX 还有一个更重要的特性,所有 JSX 元素都以一个左尖括号紧跟一个标识符开头:

  • 如果这个标识符的第一个字母小写​,则这个标签会以字符串形式传给 createElement()
  • 如果这个标识符的第一个字母大写,那么它就会被当成真正的标识符,最终传给 createElement() 的第一个参数是该标识符的 JavaScript 值

这意味着 JSX 表达式 <Math/> 编译后的 JavaScript 代码会把全局 Math 对象传给 React.createElement()

对于 React 而言,给 createElement() 第一个参数传非字符串值的能力是创建组件所必需的。组件是一种用简单 JSX 表达式(使用大写组件名)来表达更复杂表达式的方式(使用小写 HTML 标签名):

在 React 中定义组件的最简单方式就是写一个函数,让它接收一个 props 对象参数,并返回一个 JSX 表达式。props 对象就是一个简单的 JavaScript 对象,表示属性值,与传给 createElement() 第二个参数的对象一样。

1
2
3
4
5
6
7
8
9
function Sidebar(props) {
return (
<div>
<h1>{props.title}</h1>
{props.drawLine && <hr/>}
<p>{props.content}</p>
</div>
)
}

现在 Sidebar 就是一个 React 组件了,可以在 JSX 表达式中用来替换 HTML 标签名了:

1
let sidebar = <Sidebar title="Something snappy" content="Something wise" />

它会编译为如下调用:

1
let sidebar = React.createElement(Sidebar, {title: "Something snappy", content: "Something wise"});

对于这个简单的 JSX 表达式,React 在渲染时会把第二个参数 Props 对象 传给第一个参数 Sidebar() 函数​,并使用这个函数返回的 JSX 表达式替换 <Sidebar> 表达式。

使用 Flow 检查类型

Flow 也是一个语言扩展,让我们可以为 JavaScript 代码添加类型注解,同时它也是一个检查 JavaScript 代码中类型错误(包括有注解和无注解)的工具:

  • 要使用 Flow,需要一开始就使用 Flow 语言扩展给代码添加类型注解
  • 然后可以运行 Flow 工具分析代码、报告类型错误
  • 等修复错误并准备好运行代码后,可以使用 Babel(作为打包流程中自动执行的一环)从代码中剥离 Flow 类型注解

TypeScript 是 Flow 的一个非常流行的替代品。TypeScript 也是一种 JavaScript 扩展,但它除了类型还添加了其他语言特性。TypeScript 编译器 tsc 负责把 TypeScript 程序编译为 JavaScript 程序,在此期间会像 Flow 那样分析并报告类型错误。tsc 不是 Babel 插件,而是一个独立的编译器。TypeScript 中简单的类型注解通常与 Flow 中同样的注解写法相同。对于更高级的类型注解,两种扩展语法存在差异,但它们的意图和价值相同。

TypeScript 是 2012 年发布的,早于 ES6,Flow 是一个相对窄的语言扩展,只给 JavaScript 增加了类型注解。TypeScript 则是一个经过良好设计的新语言。顾名思义,为 JavaScript 添加类型是 TypeScript 的主要目的,也是人们今天使用它的原因。但类型并不是 TypeScript 给 JavaScript 添加的唯一特性。

使用 Flow 需要一定的投入,但我发现对大中型项目来说,这些额外的努力是值得的。主要你对 JavaScript 这门语言有了信心,在 JavaScript 项目中引入 Flow 肯定可以让你的编程技能更上一层楼。

安装和运行 Flow

可以使用包管理工具 npm 来安装 Flow,例如 npm install -g flow-binnpm install --save-dev flow-bin

  • 如果使用 -g 全局安装,则直接通过 flow 命令来运行
  • 如果使用 --save-dev 在项目中局部安装,那可以使用 npx flow 来运行它

在使用 Flow 做类型检查前,第一次在项目根目录运行 flow init 会创建一个 .flowconfig 配置文件。Flow 需要通过它知道你的项目根目录在哪里。

运行 Flow 时,它会找到项目中所有的 JavaScript 源代码,但它只会针对包含了 // @flow 顶部注释的文件进行类型检查。

即使仅仅在文件顶部加上 // @flow 注释,Flow 也能够发现你代码中的错误。即使没有使用 Flow 语言扩展,也没有在代码中添加类型注解,Flow 类型检查器仍然能够推断程序中的值,并在发现不一致时给出警告。

使用类型注解

在声明 JavaScript 变量时,可以给变量添加 Flow 类型注解,只要在变量名后面加上冒号和类型即可:

1
2
3
let message: string = "Hello";
let flag: boolean = false;
let n: number = 42;

即使不给这些变量添加注解,Flow 也会知道它们的类型。它知道赋给变量的值,然后一直跟踪。不过,要是添加了类型注解,Flow 就既知道变量类型,也知道你希望该变量始终保持该类型。因此如果使用类型注解,Flow 会在你给变量赋不同类型的值时标示出错误。如果你习惯使用前在函数顶部声明所有变量,那么类型注解会特别有用。

  • 函数参数的类型注解与变量的注解类似,也是在参数名后面加上冒号和类型名
  • 在注解函数时,通常也需要注解函数返回值的类型。返回值类型放在结尾圆括号与开头花括号之间。不返回值的函数使用 Flow 类型 void
  • 箭头函数也可以增加类型注解
  • JavaScript 值 null 对应 Flow 类型 null,而 JavaScript 值 undefined 对应 Flow 类型 void
  • 如果想让 null 和 undefined 成为变量或函数参数的合法值,只要在类型前面加个问号即可
  • 问号也可以出现在参数名后面,表示该参数本身可选
1
2
3
4
5
function size(s: string): number {
return s.length;
}

const size = (s: string): number => s.length;

除了原始类型之外,Flow 也支持 JavaScript 的所有内置类,允许使用它们的类名作为类型。例如:

1
2
3
export function dateMatches(d: Date, p: RegExp): boolean {
return p.test(d.toISOString())
}

如果使用 class 关键字定义自己的类,那些类也会自动变成有效的 Flow 类型。不过为了使用它们,Flow 要求你必须在类中使用类型注解。特别是,类的每个属性必须有自己的类型声明。

如下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// @flow

export default class Complex {
i: number;
r: number;
static i: Complex;

constructor(real: number, imag: number) {
this.r = r;
this.i = i;
}

add(that: Complex): Complex {
return new Complex(this.r + that.r, this.i + that.i);
}
}

Complex.i = new Complex(0, 1);

对象

描述对象的 Flow 类型看起来很像一个对象字面量,只不过属性值都变成了属性类型。

1
2
3
export default function distance(point: {x:number, y:number}): number {
return Math.hypot(point.x, point.y);
}

在上面的代码中,{x:number, y:number} 是一种 Flow 类型,与其他类型一样,也可以在它的前面加上问号表示允许 null 和 undefined。

  • 如果对象类型中的属性没有标记为可选,那它就是必需的。Flow 会在实际值中不存在对应属性时报错
  • 正常情况下 Flow 会允许出现额外的属性
  • 如果想让 Flow 严格按照类型注解中出现的属性检查,可以通过在花括号中添加一对竖线来声明确切的对象类型
1
{|x:number, y:number|}
  • JavaScript 的对象有时候会被用作字典或字符串到值的映射。像这样使用时,属性名提前是不知道的,无法用 Flow 类型来声明。如果这样使用对象,仍然可以使用 Flow 描述这个数据结构。例如:
1
2
3
4
const cidyLocations: {[string]: {longitude: number, latitude: number}} = {
"Seattle": {longitude: -122.3321, latitude: 47.6062},
}
export default cidyLocations;

类型别名

对象可能有很多属性,而描述这样一个对象的 Flow 类型可能会很长,输入起来费时间。可以给复杂的 Flow 类型命名,而且事实上,Flow 使用 type 关键字来定义类型。在 type 关键字后面要写标识符、等于号和 Flow 类型。定义了这样一个类型后,标识符就成为该类型的别名。

1
2
3
4
5
6
7
// @flow

export type Point = {x: number, y: number};

export default function distance(point: Point): number {
return Math.hypot(point.x, point.y);
}

这里导出了 Point 类型,如果其他模块也想使用该类型定义,可以使用 import type Point from './distance.js' 导入。import type 是 Flow 语言扩展,并非真正的 JavaScript 导入指令。类型导入和导出由 Flow 类型检查器使用,但与其他所有 Flow 语言扩展一样,它们会在代码实际运行之前被剥离掉。

数组

Flow 中描述数组的类型是一个复合类型,其中也包含数组元素的类型。

1
2
3
4
function average(data: Array<number>): number {
...
}
average([1, 2, "three"]);
  • Flow 中的数组类型是 Array 后跟一对尖括号,尖括号中是元素类型。也可以用元素类型后跟一对方括号来表示数组类型。例如 number[] 等价于 Array<number>
  • Flow 有一种不同的语法,用于描述一种元组(tuple)类型,即一个有固定数量元素的数组,每个元素可以是不同的类型。要表示元组类型,只需简单地写出每个元素的类型,以逗号分隔,然后把它们全部放到一对方括号中即可,例如 [number, string]
  • Array<mixed> 表示数组元素可以是任意类型,但是 Flow 仍然会在对数组元素执行不安全操作之前使用 typeof 或其他测试手段来确定元素的类型。而 Array<any> 则完全放弃对数组元素的类型检查

其他参数化类型

在把一个值注解为 Array 时,Flow 要求在尖括号中指定数组元素的类型。这个额外的类型称为类型参数,而 Array 也不是唯一可以参数化的 JavaScript 类。

  • Set 类型也是使用尖括号中包含类型参数,指定集合中值的类型(如果集合可以包含多种类型值,类型参数也可以是 mixed 或 any)。例如 Set<number>
  • Map 是另一个参数化类型。但 Map 必须指定两种类型参数,即键的类型和值的类型。例如 Map<string, number>

Flow 也允许你为自己的类定义类型参数。例如

1
2
3
4
5
6
7
8
9
10
11
export class Result<E, V> {
error: ?E;
value: ?V;

constructor(e: ?E, v: ?V) {
this.error = e;
this.value = v;
}
}

let result: Result<Error, Set<string>>;

甚至可以为函数定义类型参数,例如:

1
2
3
4
5
6
7
8
9
10
function zip<A,B>(a: Array<A>, b: Array<B>): Array<[?A, ?B]> {
let result: Array<[?A, ?B]>= [];
let len = Math.max(a.length, b.length);
for (let i = 0; i < len; i++) {
result.push([a[i], b[i]]);
}
return result;
}

let pairs: Array<[number, string]> = zip([1, 2, 3], ["one", "two"]);

只读类型

Flow 定义了一些特殊的参数化 实用类型​,这些类型的名字以 $ 开头。大多数这种类型都有一些高级使用场景,其中有两个在实践中是非常有用的:

  • 如果有一个对象类型 T,你希望得到该类型的只读版本,可以写成 $ReadOnly<T>
  • 类似地,可以用 $ReadOnlyArray<T> 描述一个元素类型为 T 的只读数组

使用这些类型能够让我们发现由于意外修改导致的隐患。如果我们要写一个函数,接收一个对象或数组参数,并且不会修改任何对象属性或数组元素,那么就可以把这个函数参数注解为一种 Flow 的只读类型。

1
2
3
4
5
6
7
type Point = {x: number, y: number};

function distance(p: $ReadOnly<Point>): number {
return Math.hypot(p.x, p.y);
}

let p: Point = {x: 1, y: 2};

函数

已经讲解了如何给函数的参数和返回值添加类型注解,但如果函数的某个参数本身又是函数,还需要指定该函数参数的类型。要通过 Flow 表示一个函数类型,就得把每个参数的类型写下来,以逗号分隔,用圆括号括起来,后面再跟一个箭头和函数的返回类型。

1
2
3
4
5
export type FetchTextCallback = (?Error, ?number, ?string) => void;

export default function fetchText(url: string, callback: FetchTextCallback): void {
......
}

联合

在使用 Flow 时,如果需要一种类型能够允许数组、Set 和 Map,但不允许其他类型的值,可以使用联合(union)类型。通过列出想要的类型并以竖线分隔它们就可以表示这种类型:

1
2
3
4
5
6
7
function size(collection: Array<mixed>|Set<mixed>|Map<mixed, mixed>): number {
if (Array.isArray(collection)) {
return collection.length;
} else {
return collection.size;
}
}

? 前缀其实是给类型添加 |null|void 后缀的简写形式。

一般来说,在使用联合类型注解一个值时,Flow 在你判断完值的实际类型前是不允许使用它们的。

枚举和可区分联合

Flow 允许使用原始值字面量作为只包含那一个值的类型。例如如果写 let x:3; 则 Flow 不允许给这个变量赋 3 之外的任何值。这种字面量类型的联合很有用。但是需要注意,对于由字面量构成的类型,它只允许字面量值。Flow 在检查类型时,并不实际执行计算:

1
2
3
4
type Answer = "yes" | "no";

let a1: Answer = "yes";
let a2: Answer = "Yes".toLowerCase(); // 错误,相当于把 string 类型的值赋值给 Answer 类型

这种字面量联合类型是枚举类型的一个例子:

1
type Suit = "Hearts" | "Diamonds" | "Clubs" | "Spades";

面量类型的另一个重要应用是创建可区分联合(discriminated union)。在使用(由不同类型而非字面量构成的)联合类型时,通常需要写代码区分各种可能的类型。如果你想创建一个 Object 类型的联合,可以在每个 Object 类型中使用一个字面量类型让这些类型容易区分。如下是一个例子:

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
export type ResultMessage = {
messageType: "result",
result: Array<ReticulatedSpline>,
}

export type ErrorMessage = {
messageType: "error",
error: Error,
}

export type StatisticsMessage = {
messageType: "statistics",
value: number,
}

export type WorkerMessage = ResultMessage | ErrorMessage | StatisticsMessage;

function handleMessage(message: WorkerMessage): void {
if (message.messageType === "result") {
console.log(message.result);
} else if (message.messageType === "error") {
throw message.error;
} else if (message.messageType === "statistics") {
console.info(value);
}
}