Firede Studio

专注于研发写作工具、开源软件的个人工作室

从 Flow 到 TypeScript 迁移类型声明

2018-4-11

迁移类型声明,这是有多想不开?

因为我在 TypeScript 项目里深度使用了 GraphQL。

在使用 JavaScript 的 GraphQL 社区中,多数玩家都选择了 TypeScript,只有 Facebook 官方维护的项目政治正确的采用了 Babel + Flow 的组合,如 graphql1graphql-express 等。

问题在于,官方的 graphql 被各种项目所依赖,却只有 Flow 类型声明,没有 TypeScript 类型声明。TypeScript 用户只能用第三方的 @types/graphql2——这个包完善度很低,存在许多错误——版本也相对滞后。

在被坑了很多次之后,我决定接手,把这个 没技术含量,但却 有挺大影响(每周20万+下载量)的活儿给做了。

现在迁移的怎么样了?

目前已经上线了 @types/graphql0.13.03 版本,可以通过以下方式安装:

npm i -D @types/graphql

如果是现有项目,可能会有很多报错,这是老版本规则过于宽松(很多 any)导致的。

想了解细节,可以从这个 PR 看起:#24566

Flow 与 TypeScript 的常见差异

找了一圈之后,我没有找到 靠谱 的 Flow to TypeScript 自动转换工具,只好上了手动挡。

搞定之后,也算是有了些经验,接下来就聊聊两者的差异。

一些秒懂的差异

Flow TypeScript
mixed any
+key: string readonly key: string
key: $ReadOnly<string> key: Readonly<string>
key: $ReadOnlyArray<string> key: ReadonlyArray<string>
<T: Foo> <T extends Foo>
$keys<A> keyof A
$Values<A> A[keyof A]

更多请参考 Boris Cherny 的 flow-to-typescript

void 语义不同

  • Flow 中 void 是 JavaScript 的 undefined
  • TypeScript 中 void 是 JavaScript 的 nullundefined

Maybe Types 表达方式

Flow
// Flow 中的 Maybe types 除了所修饰的类型,还可能是 null 或 undefined。
type Foo = {
  keyA: ?string
  keyB?: ?number
}

转换为 TypeScript 时:

TypeScript
// TypeScript 没有 Maybe types 概念,附加 null 或 undefined 作为等价的定义。
interface Foo {
  // 这里我使用了 void 代替。另外,有种观点认为 void 应避免在参数中使用:
  // https://github.com/Microsoft/dtslint/blob/master/docs/void-return.md
  keyA: string | void

  // 若 keyB 可选,则已经允许值的类型为 undefined,补足 null 即可。
  keyB?: number | null
}

不好转换的 Utility Types

有不少 Flow 的定义是 Utility Types 的组合,其中有些 文档中没有的类型 也会被广泛使用(可以 参考源码),这时候可能找不到等价的转换方式,就只能“意译”了,比如:

Flow
export type ASTVisitor = Visitor<ASTKindToNode>;
export type Visitor<KindToNode, Nodes = $Values<KindToNode>> =
  | EnterLeave<
      | VisitFn<Nodes>
      | ShapeMap<KindToNode, <Node>(Node) => VisitFn<Nodes, Node>>,
    >
  | ShapeMap<
      KindToNode,
      <Node>(Node) => VisitFn<Nodes, Node> | EnterLeave<VisitFn<Nodes, Node>>,
    >;
type EnterLeave<T> = {| +enter?: T, +leave?: T |};
type ShapeMap<O, F> = $Shape<$ObjMap<O, F>>;

转换后损失了一些信息:

TypeScript
interface EnterLeave<T> {
    readonly enter?: T;
    readonly leave?: T;
}
type EnterLeaveVisitor<KindToNode, Nodes> = EnterLeave<
    VisitFn<Nodes> | { [K in keyof KindToNode]?: VisitFn<Nodes, KindToNode[K]> }
>;
type ShapeMapVisitor<KindToNode, Nodes> = {
    [K in keyof KindToNode]?: VisitFn<Nodes, KindToNode[K]> | EnterLeave<VisitFn<Nodes, KindToNode[K]>>
};
export type ASTVisitor = Visitor<ASTKindToNode>;
export type Visitor<KindToNode, Nodes = KindToNode[keyof KindToNode]> =
    | EnterLeaveVisitor<KindToNode, Nodes>
    | ShapeMapVisitor<KindToNode, Nodes>;

接下来……

🎉搞定类型声明,终于可以开心的写代码了!

我正在为 Refine 做一个底层是 SQLite,可以基于 GraphQL 建立数据模型、做增删改查的文件数据库,专注于桌面客户端和小型网站的场景。

做到一半,我发现:

  • 🤔这东西也蛮适合移动客户端场景的
  • 😕在移动端 JavaScript 有时候不太灵
  • 🤔下个版本可以用 Rust 重构一遍
  • 🤣类型声明的迁移工好像白做了!

  1. 这里指 GraphQL 的 JavaScript 参考实现,graphql-js

  2. @types/graphql 存在于 TypeScript 的官方类型定义库 DefinitelyTyped 中。

  3. 本来应该是对应 0.13.2 的,但貌似和 DefinitelyTyped 发布机制有关,三位版本从 0 开始。

赵雷 Firede

独立开发者,现居于合肥,现致力于将工程化带进写作领域。
最近对写作理论、GraphQL、Electron、Rust 很感兴趣。

Blog comments powered by Disqus.