有时程序需要同时处理不同类型的数据,其中数据的形状可能根据代码正在查看的数据类型而不同。这种类型的编程在函数式编程语言中非常常见,以至于几乎所有此类语言都提供了一种方法来
- 通过一组不相交的 case 来指定此类数据,这些 case 由“标签”区分,每个标签都与不同的属性“记录”相关联。(这些描述称为“不相交联合”或“变体”类型。)
- 通过检查标签,然后直接访问关联的属性记录来对这种数据进行 case 分析。(进行这种 case 分析的常用方法是模式匹配。)
分析或转换此类数据的程序示例包括从处理抽象语法树的编译器到可能返回异常值的运算,以及介于两者之间的更多内容!
从 Flow 0.13.1 开始,现在可以在 JavaScript 中以类型安全的方式使用这种风格进行编程。您可以定义对象类型的不相交联合,并通过切换这些对象类型中某些公共属性(称为“哨兵”)的值来对该类型的对象进行 case 分析。
Flow 用于不相交联合的语法如下
type BinaryTree =
{ kind: "leaf", value: number } |
{ kind: "branch", left: BinaryTree, right: BinaryTree }
function sumLeaves(tree: BinaryTree): number {
if (tree.kind === "leaf") {
return tree.value;
} else {
return sumLeaves(tree.left) + sumLeaves(tree.right);
}
}
问题
考虑以下函数,它根据传递给它的数据返回不同的对象
type Result = { status: string, errorCode?: number }
function getResult(op): Result {
var statusCode = op();
if (statusCode === 0) {
return { status: 'done' };
} else {
return { status: 'error', errorCode: statusCode };
}
}
结果包含一个 status
属性,该属性为 'done'
或 'error'
,以及一个可选的 errorCode
属性,当 status
为 'error'
时,该属性保存一个数字状态码。
现在,您可能尝试编写另一个函数,从结果中获取错误代码
function getErrorCode(result: Result): number {
switch (result.status) {
case 'error':
return result.errorCode;
default:
return 0;
}
}
不幸的是,此代码无法进行类型检查。Result
类型无法准确地捕获 status
属性和 errorCode
属性之间的关系。即它没有捕获当 status
属性为 'error'
时,errorCode
属性将在对象上存在且已定义。因此,Flow 认为上面的函数中的 result.errorCode
可能返回 undefined
而不是 number
。
在 0.13.1 版本之前,没有办法表达这种关系,这意味着无法检查这种简单、熟悉的习惯用法类型安全!
解决方案
从 0.13.1 版本开始,可以为 Result
编写更精确的类型,该类型更好地捕获了意图,并帮助 Flow 根据动态 ===
检查的结果缩小对象的可能形状。现在,我们可以编写
type Result = Done | Error
type Done = { status: 'done' }
type Error = { status: 'error', errorCode: number }
换句话说,我们可以明确列出结果的可能形状。这些 case 由 status
属性的值区分。请注意,这里我们使用字符串字面量类型 'done'
和 'error'
。这些与字符串 'done'
和 'error'
完全匹配,这意味着对这些值的 ===
检查足以让 Flow 缩小相应的类型 case。有了这种额外的推理,函数 getErrorCode
现在可以进行类型检查,无需对代码进行任何更改!
除了字符串字面量之外,Flow 还支持数字字面量作为单例类型,因此它们也可以用于不相交联合和 case 分析。
我们为什么构建它
不相交联合是函数式编程语言中普遍存在的几种良好编程实践的核心。在 Flow 中支持它们意味着 JavaScript 可以以类型安全的方式使用这些实践。例如,不相交联合可用于编写类型安全的 Flux 调度程序。它们也大量用于最近发布的 GraphQL 参考实现。