TypeScript
flowtype

Flowtype と TypeScript で両方動きそうなコードを書く

最初に知るべきこととして、Flow も TS も型システムのセマンティクスがよく似ている。

Redux & Typescript typed Actions with less keystrokes の 記事のかなり魔術的なコード、実は flow でも ts でもそのまま動いたりする。

自分は両方頻繁に使うので(flow寄りだが)、どういうコードを書くと手戻りが少ないか、考えながらコードを書いてるか書き出してみる。

お断り

flowtype と typescript の、特に typescript 的なベストプラクティスに反する可能性がある。
完全にコンパチにするのは不可能だが、極力似たイディオムを使う。

どれも「極力頑張る」という感じで原則禁止というわけではない。大事なのは相互にコードを持ち出す時のポータビリティ。最悪なのは型がないという状態。

あと自分は TypeScript の非標準なモジュール拡張を非常に MS 的な所作だという理由で嫌っている自覚があるので、その辺の温度感も差し引いてもらいたい。

TypeScript: strictNullChecks を有効にする

{
  "compilerOptions": {
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "alwaysStrict": true
  }
}

こっちのほうが flow の挙動に近い。

TypeScript: internal-module を使わない

// NG
module MyInternalModule.Helpers {
  export class MyClass {}
}
// OK
// helpers/MyClass.ts
export class MyClass {}

ES Modules を 使えば不要なはず。

tslint なら no-internal-module: trueno-var-require: true

TypeScript: namespace を使わない

コードは略。 internal module と同様、 ES Modules にそんなものはない。
非標準なモジュールシステムを使わないという点で一貫する方が良い。

tslint なら no-namespace: true

TypeScript: private/public を使わない

// NG
class MyClass {
  private foo: string = 'FOO'
}

// OK
class MyClass {
  foo: string = 'FOO'
}

JSにそんなものはないし、 むしろ private field は #foo になりそう。

tc39/proposal-private-fields: A Private Fields Proposal for ECMAScript

stage3 に入って、 typescript が対応したら考える。

そもそも、ここしばらく React の componentDidMount 以外で目的で class を使った記憶がない。 最近はかなりFP厨気味なコードを書いている自覚があり、 class もオブジェクトへの副作用もそもそも不要だと思ってコードを書いている。

TypeScript: enum を使わない

// NG
enum Color { RED = 'red', GREEN = 'green', BLUE = 'blue' }

// OK
type Color = 'red' | 'green' | 'blue'

union type で十分。

探したが tslint に no-enum なかった。誰もそんな発想をしていないっぽい。

FlowType: nullable syntax を使わない

// NG
let nullableValue: ?string = null

// OK
let nullableValue: string | null = null

この表現の差として下の方では undefined が弾かれてしまうが、これとは別に、自分は明示的に空である値 を代入する際は null を使うというルールを採用しているので、問題ない。

FlowType: TypeScript 共通: 組み込み型をできるだけ使わない

$Keys<T> とか keyof<T> とか使い込むと持ち出せなくなる。

これらをヘビーに使うコードはライブラリ型定義だと思われるので、そういう場合はコードの共通化を諦めて両方書いて flow-typed@types の仕組みに乗せる。

どうあがいても無理な import type と nested module

Flow は 型情報はコンパイル時に完全に消去され、インスタンスと型が明確に区別されている。主にこれが理由で enum がない。

import type {...} from '...' または import {type ...} from '...' を TypeScript で表現する方法はない。

また、逆に TypeScript の型定義で使われる表現で、次のようなものはFlowで上手く表現する方法がない。

module A {
  module B {
    export class C {...}
  }
}

これは flow の 型定義構文の module が入れ子にならないので、一旦複数の module に分解し直さないと同等の表現ができない。昔これが理由で flow への .d.ts 変換器を書くのを諦めた。

おまけ: 両者の Pros/Cons

Flow Pros: 導入コストが本当に低い
Flow Cons: flow server の プロセスが頻繁に行方不明になる / 追っていこうにも ocaml でソースが辛い

TS Pros: IDEが安定している
TS Cons: 非標準な割れ窓が沢山ある / スクラッチ以外の導入が困難