🧊

0からはじめるFlow Part.2

前回のPart.1では導入編ということで環境を整えるところまで。
今回からは各ファイルに実際に型をつけていくところを。

そしてその過程でハマったものについて・・・。

最初に結論

Flowの導入を迷ってるそこのあなた!

それははじめたてのプロジェクトですか?そうでないなら要注意。
既存のプロジェクトに導入する場合、

  • 元コードを型安全なFlowに怒られないコードに修正していくのがそもそも大変だったり
  • Classプロパティに型をつけると突然Babelのコンパイルが失敗するようになったり(Part.1を参照
  • 大した処理がない、更新頻度が低い、...etcなど、型の恩恵が薄くただ徒労感を感じたり
  • そもそも困っても世間に情報があまり見当たらなくてつらかったり

なんしかまぁ色々と起こるので、それなりに大変です。

俺は結局最初のモチベーションであったウデマエアーカイブへの導入は挫折。
でもやりかけで終わりたくなかったので、気合でスーパーイカメーカーに導入しました。

ただ新規のプロジェクトならすべからく恩恵に預かれると思うので、オススメです。
さてさて、以下はそんな過程で培った知見たちです。

まず最初にドキュメントを

そもそも型と無縁な暮らしを送ってきたJSerが、ドキュメントも読まずに何ができるんだという話です。

ついつい手を動かしたくなるけども、ぐっとこらえて少なくとも公式のドキュメントは全部読む。
手戻りしたくないならなおさら読む。

Flow | Quick Reference

このLANGUAGE REFERENCEは一通り目を通すべし。
型をつけるための記述方法にどんなバリエーションがあるかを最初に知っておく。


次はココ。

flow/lib at master · facebook/flow · GitHub

Flowが用意してくれてる型定義の一覧。
ブラウザネイティブのメソッド呼んだときとか、Polyfillに型を当てる時とかに有用。

自分で定義しなくても、React関連のライブラリは一部ココで既に定義されてたりするんやなーってのを見ておく。


最後のコレは知ってなくてもなんとかなる。

Advanced features in Flow - sitr.us
flow/type_annotation.ml at master · facebook/flow · GitHub

Flowにはなんと、ドキュメントにのってない機能がある!(後でちょっとだけ紹介するけど

最初のがそれについて書かれた記事(しかし1年前)で、コードでそれが定義されてるらしき部分。
この記事にも載ってないやつとか定義されてたりするけど、これ使いこなしてる人はいるんやろうか・・。

型の定義について

require|importしてるライブラリに型をつける

`npm`で入れたコード自体は最初に`[ignore]`してFlowのターゲットから外した。
けど、それを使うタイミングはまた別の話で、使うものは適当に型を付ける必要がある。

declare module 'module-name' {
  declare var exports: any;
}

もちろんきっちり型指定してもいいけど、エラー回避するだけなら最低限はコレで良い。

型のimport

`.flowconfig`の`[libs]`で指定したファイルで定義した型であれば、別にどこのソースからでも使える。
どっかのサンプルでいちいち全部`require|import`してるのを見た気がするけど、そんなことしなくてよろしい。

ネイティブのAPIの返り値がわからんとき

たとえば`localStorage.getItem()`とか。

最初に紹介したリンクのGitHubのやつを見て、localStorageなら、 https://github.com/facebook/flow/blob/master/lib/dom.js に書いてあるようにする。

// あればstring、ないならnullなので ? がいる
const foo: ?string = localStorage.getItem('foo');

そのほかPolyfillのライブラリ系はだいたい探せば書いてある。
`Object$Assign`とか。

const canvas: HTMLCanvasElement = document.createElement('canvas');
const ctx: ?CanvasRenderingContext2D = canvas.getContext('2d'); // ? が重要
if (ctx) {
  // ...
}

このcanvasのにだいぶハマった。

Destructuring

便利なあいつに型を付けたいとき。

const A = { a: '1', b: 2, c: true };
const { a, b, c }: { a: string, b: number, c: boolean } = A;

// コレはダメ
// const { a: string, b: number, c: boolean } = A;

って具合に後ろに型をつける。
公式のDestructuringのページに書いてないからねコレ!

ついでに、普通のオブジェクトに型をつけるときは、

const A: {
  a: string, b: number, c: boolean,
} = { a: '1', b: 2, c: true };

とまあ型と値がさっきと逆になるので注意。

Typeエイリアス

FluxのActionとか、似たようなデータの塊を共通の型にまとめたい時に使える。
`class`に対する`interface`みたいな。

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

type Rank =
  | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
  | "Jack"
  | "Queen"
  | "King"
  | "Ace";

type Card = {
  suit: Suit,
  rank: Rank,
}

こうしておくと、

const c1: Card = { suit: 'Spades', rank: 'Ace' }; // ok
const c2: Card = { suit: 'Spades' }; // ng

これはよく使う機能なはず。

$Shape とかいうドキュメントには載ってない機能

もうちょい厳密にオブジェクトのプロパティを縛りたい場合に使う・・?

const c1: Card = { suit: 'Spades', rank: 2, other: 1 }; // ok
const c2: $Shape<Card> = { suit: 'Spades', rank: 2, other: 1 }; // ng

`$Keys`とかいうのもある

おなじくドキュメントには載ってないけどこう書ける。

const TYPES = {
  "Diamonds": "Diamonds",
  "Clubs":    "Clubs",
  "Hearts":   "Hearts",
  "Spades":   "Spades"
};

type Types = $Keys<typeof TYPES>;

としておいて

const type1: Types = 'Diamonds'; // ok
const type2: Types = 'Fooo';     // ng

型定義って・・・普通にjsと組合せられるんか!!!

てことはこれ、ファイルまたいで書けたら割といいのでは???って思いますよねー。
そこについては後述。

Flowに怒られるJavaScript

オブジェクトに数値キーはダメ

// 元コード
const CONST = {
  1: 'foo',
  2: 'bar',
};

// なおした
const CONST = {
  '1': 'foo',
  '2': 'bar',
};

ダメったらダメ。

ビット演算子でキャストするのダメ

// 元コード
function foo(val) {
  val = val|0;
  // ...
}

// 型付けた(ら怒られた)
function foo(_val: string) {
  const val: number = _val|0;
  // ...
}

// なおした
function foo(_val: string) {
  let val: number = parseInt(_val, 10);
  val = isNaN(val) ? 0 : val;
}

うへーめんどくせぇー横着すんなってことですかね。

Dynamic Type Tests

こういう時に使う。

// ev.targetはEventTargetでもあるけど、`value`があるのはそのターゲットであるHTMLInputElement
onChangeInput(ev: Event) {
  const action: SetTextAction = {
    target: this.props.partsName,
    text:   ev.target.value // ココでエラー
  };
  this.dispatch('set:text', action);
}

// よって、Dynamic Type Testsして回避する
onChangeInput(ev: Event) {
  if (ev.target instanceof HTMLInputElement) {
    const action: SetTextAction = {
      target: this.props.partsName,
      text:   ev.target.value
    };
    this.dispatch('set:text', action);
  }
}

面倒くさいって思ってはいけない・・これが型をつけるということ・・。

React関連

Reactコンポーネント

class Foo extends React.Component {
  // stateも同じように
  props: {
    p1: string,
  };
  // staticならstaticってつける
  onClickFoo: () => void;
  

  // 以下は今まで通り
  onClickFoo(ev: Event) => {
    // ...
  }
  
  render() {
    const { p1, } = this.props;
    return (
      <button onClick={this.onClickFoo}>{p1}!</button>
    );
  }
}

Functionalコンポーネント

const Foo: Function = ({
  props,
}: {
  props: string;
}): ?React$Element => {
  if (props === 'noop') {
    return null;
  }

  return (
    <div>Flow with {props}</div>
  );
}

なんかごちゃごちゃしてきたなー・・・。

わからないこと

そもそも型に不慣れ

効率的な・より厳密な型の付け方がわからない。
あのドキュメントに載ってない機能の使いドコロとか。
そもそも全ての宣言に型をつける必要はあるのかとか。
ジェネリクスなんていつ使ったら良いんだ感がすごい。

まぁ必要に応じて学べば良いとは思うものの、何で勉強するもの?他の型のある言語から来た人はサラッといけるんかね?

型をどこに書くか

アノテーションはコードの中に書くしかないと思う。
ただエイリアスとかまでコード側に書く・・?別のところにまとめたいよね・・?

どっちがメジャーなの・・?

CONSTを型側で参照する

定数は定数で、コード側にあってしかるべきって思うはず。
`[libs]`で指定した型定義のファイルでは`import`とか使えないし、どうにかならんかと画策したところ。

  • `[libs]`で定数を`export`してるモジュールを指定しちゃう
  • すると、`[libs]`配下でもその変数が見えるようになる
  • ただし、本体コード側で指定したモジュールの存在が消され、Flowとしてはエラーになる
  • でもBabel的にはコンパイルできるので、コードは動く

といった気持ち悪い状態でよければなんとかできそう。
気持ち悪い。

定数をエクスポートしてるファイルで、一緒に型もエクスポートすれば、違う場所から型インポートができるようになるって話を聞いた。
やるならこっちのが良いですな!

es6-promiseの型

declare module 'es6-promise' {
  decalre class Promise extends Promise {}
}

こうじゃない・・?

inconsistent use of library definitions module.
*** Recursion limit exceeded ***

とか言われる。

extends React.Componentしたクラスで

コンポーネント間のpropsをチェックしたい場合。
というか、チェックしたい場合しかないと思うけど・・。

// P.js
const P = module.exports = ({ p1 }: { p1: string }) => {
  return (
    <div>{p1} is string!</div>
  );
};

// App.js
const P = require('./p.jsx');
const A = () => {
  return (
    <P p1={123} /> /* p1 はstring!! */
  );
};

この便利なやつですが、

  • 普通にReact.Componentを使ってるものには効く
  • ただスーパーイカメーカーでは一部で`mizchi/flumpt`を使ってる
  • すると↑のチェックが効かない
declare module 'flumpt' {
  declare class Component extends React$Component {
    dispatch: (eventName: string, ...args: any[]) => boolean;
  }
}

こういう型にしてもダメだったのでお手上げ中。
その他のチェックは効いてるっぽいんやけども。

クラスにつける型

class Foo extends React.Component {
  // コレいる?
  onClickCancel: () => void;

  constructor() {
    super();
    this.onClickCancel = this.onClickCancel.bind(this);
  }

  // ココで型つけてるやん!
  onClickCancel(ev: Event): void {
    ev.preventDefault();
  }
}

ちなみに、`constructor()`でやってる`bind()`の処理をやらない場合は、怒られないです。

おわりに

紆余曲折あったものの、Flow良いと思います!
やはり型があることの恩恵を一部でも体感できたのがデカい。

  • 型推論がすごい
  • typoとかもすぐわかるし <- 重要
  • 型の知識がつくとDOMの仕様書とかも読みやすくなる
  • 変数名から型を匂わせる記載が消えてすっきり
  • コード読む方もわかりやすくなるはず

少なくともいつまでたってもコンパイルが遅いTypeScriptに行くよりは、Flowの方がいいかなーと個人的には。

にしても全然情報無いのはなんで・・?
誰も使ってないのか、使ってるけど内緒にしてるのか、はたまた・・。