Flowtypeを使ってReactComponentで型安全を手に入れる

  • 61
    いいね
  • 0
    コメント

2017/1/8 追記
※ この記事の内容は古い(React.PropTypesへの型チェックなどはすでにundocumentedになっている)ので、 http://qiita.com/joe-re/items/d6fd262a8c6017f41e22 を参照してください。

Flowtype is 何

http://flowtype.org/

Flow is a static type checker, designed to find type errors in JavaScript programs:

とある通り、JavaScriptの世界に静的な型チェックを導入するものです。

/* @flow */
function foo(x) {
  return x * 10;
}
foo('Hello, world!');

というコードは、foo関数の実行時にstringを渡しています。
しかしfoo関数は、引数(string)と10(number)との積を求めてしまっているので、通常成り立ちません。

そのため、flowは下記のエラーを出力します。

hello.js:5:5,19: string
This type is incompatible with
  hello.js:3:10,15: number

この記事は、flowtypeをReactComponentに適用して、型安全を手に入れてみよう!
という趣旨で書きます。

Flowのバージョン

0.19.1

Reactの公式チュートリアルを適当に書き直して、それっぽいsample作った

https://github.com/joe-re/flowtype-react-sample

とりあえずbabelでes2015で書けるようにして、browserifyでビルドするようにしています。
ビルド時のflowtypeの除去はBabelがやってくれます。

チェックアウトしたら

$ npm install
$ npm run build
$ open index.html # ブラウザでindex.htmlを開ければ方法は何でも良い

で動きます。

Component間で共通するObjectを定義する

http://flowtype.org/docs/objects.html

Component間で共通するObjectのpropertyを定義します。
今回の場合は、Commentです。

src/types.js
/* @flow */

export type Comment = {
  id: number;
  author: string;
  text: string;
};

idがあって、投稿主があって、内容がある。典型的なCommentのObjectです。

下記のように記述すると、この型を読み込むことができます。

import type { Comment } from '../types';

定義した型は、このように使えます。

src/components/comment_box.js
export default class CommentBox extends React.Component {
  //...省略
  handleCommentSubmit(comment: Comment) {
    comment.id = ++count;
    this.setState({ comments: this.state.comments.concat(comment) });
  }
  //...省略
};

ここではcallbackで受け取ったcommentに型をつけています。
今回はサーバを実装していないので、便宜的にこのhandlerの中でidをincrementしてセットしています。

例えばこのidのincrementしているところを、

comment.count = ++count;

に変えるとエラーになります。

実行結果.log
$ flow
src/components/comment_box.js:23
 23:     comment.count = ++count;
                 ^^^^^ property `count`. Property not found in
 23:     comment.count = ++count;
         ^^^^^^^ object type

期待通り、countなんてないよ!って言ってくれてますね。
賢い!

これを使うと、Propsバケツリレー中の値が同じであることを保証できたり、React + Fluxにおいては、Storeが渡した値と、ReactComponentで受け取った値が同じものであることも明示的にできたりして良い感じです。

ReactComponentのStateとPropsを定義する

React.ComponentのPropertyの型を定義します。

src/components/comment_box.js
import type { Comment } from '../types';

let count: number = 0;

type State = {
  comments: Array<Comment>;
};

export default class CommentBox extends React.Component {
  state: State;

  constructor() {
    super();
    this.state = { comments: [] };
  }

  handleCommentSubmit(comment: Comment) {
    comment.id = ++count;
    this.setState({ comments: this.state.comments.concat(comment) });
  }
  //...省略
}

ここでは、CommentBoxのStateを下記のように定義しています。

type State = {
  comments: Array<Comment>;
};

これを、CommentBoxの持つStateのPropertyとして定義します。
classの直下に property名: 型定義 と書くことで定義できます。

export default class CommentBox extends React.Component {
  state: State;

これでstateの型定義ができました。

constructorを↓のように変えると、ちゃんとエラーを吐いてくれます。

  constructor() {
    super();
    this.state = { a: 'aaaaaa' };
  }
実行結果.log
$ flow
src/components/comment_box.js:19
 19:     this.state = { a: 'aaaaaa' };
         ^^^^^^^^^^ assignment of property `state`
 15:   state: State;
              ^^^^^ property `comments`. Property not found in
 19:     this.state = { a: 'aaaaaa' };
                      ^^^^^^^^^^^^^^^ object literal

setStateを変えてもちゃんとエラーになってくれて、これまた賢いです。

  handleCommentSubmit(comment: Comment) {
    comment.id = ++count;
    this.setState({ b: this.state.comments.concat(comment) });
  }
実行結果.log
$ flow                                                                                                                   *[master]
src/components/comment_box.js:24
 24:     this.setState({ b: this.state.comments.concat(comment) });
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ call of method `setState`
 24:     this.setState({ b: this.state.comments.concat(comment) });
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ property `b` of object literal. Property not found in
 15:   state: State;
              ^^^^^ object type

もちろんPropsも同じように定義できます。

src/components/comment_list.js
type Props = {
  comments: Array<Comment>
}

export default class CommentList extends React.Component{
  props: Props;

  render(): ReactElement {
    const commentNodes = this.props.comments.map((comment) => {
      return (
        <CommentItem comment={comment} key={comment.id} />
      );
    });
    return (
      <div className="commentList">{commentNodes}</div>
    );
  }
}

React.PropTypesに型チェックを適用してみる

下記のドキュメントに記載がある機能で、FlowtypeはReact.PropTypesを静的なチェックを可能にしてくれます。

http://flowtype.org/docs/react-example.html#property-use

結論から先に言うと、残念ながらうまく動きませんでした

この辺のissueなのかな…。
https://github.com/facebook/flow/issues/1158

src/components/comment_form.js
/* @flow */

import React, { PropTypes } from 'react';

export default class CommentForm extends React.Component {
  // 中略
  render(): ReactElement {
    return (
      <form className="commentForm" onSubmit={this.handleSubmit.bind(this)}>
        <input
          type="text"
          placeholder="Your name"
          value={this.state.author}
          onChange={this.handleAuthorChange.bind(this)}
        />
        <input
          type="text"
          placeholder="Say something..."
          value={this.state.text}
          onChange={this.handleTextChange.bind(this)}
        />
        <input type="submit" value="Post" />
      </form>
    );
  }
}

CommentForm.propTypes = {
  onCommentSubmit: PropTypes.func.isRequired
};

こんな感じでCommentFormを定義して、その呼び出し元が↓。

src/components/comment_box.js
/* @flow */

import React from 'react';
import CommentForm from './comment_form';
import CommentList from './comment_list';
import type { Comment } from '../types';

let count: number = 0;

type State = {
  comments: Array<Comment>;
};

export default class CommentBox extends React.Component<{}, {}, State> {
  // 中略 

  render(): ReactElement {
    return(
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentForm onCommentSubmit={this.handleCommentSubmit.bind(this)} />
        <CommentList comments={this.state.comments} />
      </div>
    );
  }
};

この場合に、例えばrenderの中でCommentFormを呼び出しているところを、

<CommentForm onCommentSubmit={'aaaaaa'} />

とかに変えたら、Functionではなくstringが渡ってきたぞ!ってエラーを吐いてほしいのだけど、残念ながらエラーにはならなかった。

$ flow
No errors!

何が原因なのかは正直よく分からないのだけど、前述の通り、ReactComponentのStateとPropsは定義できるので、コンパイル時の型安全という意味では、まぁなくてもいいかな、という気がしてます。
(動かなかった時点の状態でサンプルでは0.0.1でタグ切っておいたので、もし試したい方がいたらどうぞ。原因が分かったら教えてください :bow: )

所感

flowが発表されたのがちょうど1年ぐらい前で、1年経てばそれなりにノウハウがたまって情報も集めやすくなってるかなーと思っていたのですが、残念ながらまだそんなことはなかったです。

ただ僕はFlowtypeの思想は好きで、期待は大きく持っています。

フロントエンドにおいて、ある程度モダンな環境を求めるならばビルドをすることが前提になってしまったこの世界線では、開発プロセスの中で何度もイテレートすることになるbuildやlintにかける時間を高速にすることが開発効率を上げる上で非常に重要です。

その点において、flowは1度起動時にチェックした後は、独自に依存性を解決して差分のみのチェックを行うので爆速です。

個人的にAltJSを使わなくて済むなら極力使いたくないと思っているので、syntax checkのみを提供するという姿勢も好印象です。

Githubの議論を見ていると、ES7のsyntaxへの対応の議論があったりして、未来への意欲も十分に感じます。

が、いかんせんProductionに使うにはまだ少しこなれてないかなー、という印象もあります。
全員がフロントエンドへのモチベーションが高くて、俺が道を切り開くぜ!という意欲に満ちているプロジェクトなら良いですが、はまると解決するまでに時間がかかる可能性が高いので、ある程度余裕がないと難しいと思います。

今回紹介したように、ReactComponentだけに適用するなら小さい世界でできるので、まずはここから始めてみるのがいいのかも知れません。

この投稿は React.js Advent Calendar 201512日目の記事です。