undefined

bokuweb.me

flowtypeを試してみる

最初に

この記事はflowtype導入の手順紹介というより、自分の作業ログに近いものです。flowtypeって何?ってところも含めて以下に紹介する記事を見たほうがわかりやすいと思いますので、参照してください。

今回試すにあたって、参考にした記事。

qiita.com

qiita.com

joe-re.hatenablog.com

qiita.com

動機

自分の観測範囲内で「flowtypeいいよ!」って話しをよく聴くようになり、試してみることにした。 自分の場合はだが、主な動機としてはReactのpropTypes頑張って書く割に得られる恩恵少ないというのがあってflowtypeであれば、それを改善しつつ、部分的に適用することができる。

新規プロジェクトで あればTypescriptを採用するなどの選択肢を取ることが可能であるが、既存プロジェクトの場合はそうはいかない。だけど、flowtypeであれば既存のプロジェクトにも型の恩恵を付加することができる。

また上でも紹介している以下の記事内にFLOW SOUNDNESS, NO RUNTIME EXCEPTIONS AS GOALとあって、これは導入しない手はないんじゃないか?と思い今に至る。実行時例外のない世界に住んでみたい。

joe-re.hatenablog.com

作業ログ

導入

以下で入る。

npm i -D flow-bin babel-plugin-transform-flow-strip-types

現状のバージョンがflow-bin@0.27.0babel-plugin-transform-flow-strip-types@6.8.0flow-binが本体で、babel-plugin-transform-flow-strip-typesがトランスパイル時にflow type annotationを除去してくれるもの。

package.jsonにflowを追加する。

  • package.json
"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "flow": "flow",
  "watch": "watchify --extension=js -o public/dist/bundle.js public/src/index.js",
  "start": "node server.js & npm run watch"
},

.babelrcにtransform pluginを追加する。

  • .babelrc
{
  "presets": ["react", "es2015", "stage-1"],
  "plugins": ["babel-plugin-transform-flow-strip-types"]
}

.flowconfigを用意する

今後必要な設定は.flowconfigに追記していく。 ひとまずは以下のような感じ。node_modules以下対象外としておく。

  • .flowconfig
[ignore]
.*/node_modules/.*

[include]

[libs]

[options]

ひとまずの導入はこんなところ。早速動作させてみる。

動作テスト

公式の例となっている以下のようなファイルを用意。

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

チェックをかけてみる

$ npm run flow

test.js:5
  5: foo('Hello, world!');
     ^^^^^^^^^^^^^^^^^^^^ function call
  3:   return x * 10;
              ^ string. This type is incompatible with
  3:   return x * 10;
              ^^^^^^ number

Found 1 error

良さそう。

サンプルへの適用

以前Reactのチュートリアル(コメントボックス)にReduxを使ったサンプルを作ってみたことがあるんだけど、そのプロジェクトに対し部分的にflowtypeの適用を試みてみる。その成果物となるものは以下。

github.com

コメント送信部への適用

まずはコメント送信部、public/src/components/comment-form.jsへの適用を試みてみる。

Comment型を定義する。この定義は他のコンポーネントなどでも使用するので別ファイルに定義し、importするものとする。

  • public/src/types.js
/* @flow */

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

以下のように使用する。

  • public/src/components/comment-form.js
/* @flow */

import React, { Component, PropTypes } from 'react';
import type { Comment } from '../types';

... 省略 ...
  handleSubmit(e) {
    e.preventDefault();
    const author = this.author.value.trim();
    const text = this.text.value.trim();
    if (!text || !author) return;
    const comment: Comment = { author, text };
    this.props.onCommentSubmit(comment);
    this.author.value = '';
    this.text.value = '';
  }

この時点でチェックをかけると(当然)めちゃめちゃに怒られる。足りない情報を埋めていき、ひとまず以下のような形になった。その際以下のものをよく参照した。(その他まとまっているものなどあれば教えていただけるとうれしいです。)

Flow type cheat sheet - SaltyCrane Blog

github.com

/* @flow */

import React, { Component, PropTypes } from 'react';
import type { Comment } from '../types';

export default class CommentForm extends Component {
  author: HTMLInputElement;
  text: HTMLInputElement;

  handleSubmit(e: Event) {
    e.preventDefault();
    const author = this.author.value.trim();
    const text = this.text.value.trim();
    if (!text || !author) return;
    const comment: Comment = { author, text };
    this.props.onCommentSubmit(comment);
    this.author.value = '';
    this.text.value = '';
  }

  render() {
    return (
      <form className="commentForm" onSubmit={this.handleSubmit.bind(this)}>
        <input type="text" placeholder="Your name" ref={c => {this.author = c;}} />
        <input type="text" placeholder="Say something..." ref={c => {this.text = c;}} />
        <input type="submit" value="Post" />
      </form>
    );
  }
}

例えば以下のように変更すると怒ってくれる。

const comment: Comment = { author, 10 };
public/src/components/comment-form.js:15
 15:     const comment: Comment = { author, 10 };
                        ^^^^^^^ property `text`. Property not found in
 15:     const comment: Comment = { author, 10 };
                                  ^^^^^^^^^^^^^^ object literal

public/src/components/comment-form.js:15
 15:     const comment: Comment = { author, 10 };
                                            ^^ non-string literal property keys not supported


Found 2 errors

良さそう。

propTypesへの適用

メインの動機でもあるpropTypesのチェックを行ってみる。 この辺りから導入してやると、費用対効果が高そうな印象を受ける。

propTypesの定義をstatic propTypes = { ... }のように記述していると、early stage feature proposalということでオプションの設定を促される。当たり前だが、これらはbabelではなくflowtype側で解決する必要がある。具体的には、.flowconfigの[options]に以下を追記してやる。

[options]
esproposal.class_static_fields=enable

comment-formは以下のように変更した。具体的にはpropsで渡されるonCommentSubmitが 関数であることを教えている。

  • public/src/components/comment-form.js
type Props = {
  onCommentSubmit: Function,
}

export default class CommentForm extends Component {
  static propTypes = {
    onCommentSubmit: PropTypes.func.isRequired,
  };

  props: Props;
  author: HTMLInputElement;
  text: HTMLInputElement;
  ...

comment-formを使う側のコンポーネントも/* @flow */を追記しチェックの対象とする。 試しにFunctionではなくNumberを渡してみる。

  • public/src/comment-box.js
  render() {
    const { comments, saveComment } = this.props;
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList comments={comments} />
        <CommentForm onCommentSubmit={1} />

チェックをかける。

public/src/components/comment-box.js:20
 20:         <CommentForm onCommentSubmit={1} />
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ React element `CommentForm`
 20:         <CommentForm onCommentSubmit={1} />
                                           ^ number. This type is incompatible with
  7:   onCommentSubmit: Function,
                        ^^^^^^^^ function type. See: public/src/components/comment-form.js:7


Found 1 error

良さそう。ここまで書くと逆にpropTypesを書くメリットはなくなるのだろうか?propTypesを拾ってくれるstyleguideのために書くくらいだろうか?

Reduxへの適用

次にRedux側にも適用してみる。外部ライブラリの型定義はflow-typedで管理するっぽい。

qiita.com

flow-typedのインストール

自分は基本的にはローカルに入れたいんだけど、今回ローカルにいれると面倒そうなのでグローバルにいれることにした。

npm install -g flow-typed
Reduxの型定義の検索、入手

flow-typed search PACAGE_NAMEで検索をできるよう。 flow-typed search Reduxとすると以下のような結果がえられる。

$ flow-typed search redux
 * flow-typed cache not found, fetching from GitHub...done.

Found definitions:
╔═══════════════╤═════════════════╤══════════════╗
║ Name          │ Package Version │ Flow Version ║
╟───────────────┼─────────────────┼──────────────╢
║ redux-actions │ v0.9.x          │ >=v0.23.x    ║
╟───────────────┼─────────────────┼──────────────╢
║ redux         │ v3.x.x          │ >=v0.23.x    ║
╟───────────────┼─────────────────┼──────────────╢
║ redux-form    │ v5.x.x          │ >=v0.22.1    ║
╚═══════════════╧═════════════════╧══════════════╝

す、すくない。redux-xxxxというパッケージの型定義ファイルが2つしか登録されていないことになる。インストールは以下のようにする。

$ flow-typed install -f v0.27.0 redux@3.0.0

これでproject配下のflow-typed/npm/に型情報が格納されると思う。

Storeの型を定義する

インストールした定義は以下のように使用できる。以下はconfigureStoreの返り値の型がStoreであることを定義している。 当然、このままチェックをかけると怒られる。具体的にはredux-thunkredux-loggerが何なのだ?という話しになる。

/* @flow */

import type { Store, Middleware } from 'redux';
import { createStore, applyMiddleware } from 'redux';
import comment from '../reducers/comment';
import thunk from 'redux-thunk';
import createLogger from 'redux-logger';

export default function configureStore(): Store {
  const logger = createLogger();
  const createStoreWithMiddleware = applyMiddleware(
    thunk, logger
  )(createStore);
  const store: Store = createStoreWithMiddleware(comment);
  return store;
}

そのような場合は .flowconfig[libs] でmodule を定義することで import 時に解決するオブジェクトの型定義を解決してくれるらしい。 具体的には以下のように記述している。

  • .flowconfig
[ignore]
.*/node_modules/.*

[include]

[libs]
decls

[options]
esproposal.class_static_fields=enable

[libs]にdeclsディレクトリを指定しておき、decls/modules.jsに以下のようにredux-thunkredux-loggerの型を定義した。

  • decls/modules.js
declare module 'redux-thunk' {
  declare var exports: Function;
}

declare module 'redux-logger' {
  declare var exports: (options?: Object) => Function;
}

この辺りも正直合っているのかわからないところもあるのだけど、declare var exports: string;にしたら怒ってくれるので機能しているようには見える。

Action / Reducerへの適用

あとはこつこつ情報を埋めていくことになる。 actionとreducerに適用してみたものは以下のようになる。 Typescriptflowもこれまで触ったことがなく、間違っている可能性もあるのでその場合はご指摘ください。

  • public/src/actions/comment.js
/* @flow */

import * as api from '../api/api';
import type { Comment } from '../types';
import type { Dispatch } from 'redux';

export const SUBMIT_COMMENT = 'SUBMIT_COMMENT';
export const RECIEVE_COMMENTS = 'RECIEVE_COMMENTS';

export type CommentActionType =
  { type: 'SUBMIT_COMMENT', comment: Comment } |
  { type: 'RECIEVE_COMMENTS', comments: Array<Comment> };


export function submitComment(comment: Comment): CommentActionType {
  return {
    type: SUBMIT_COMMENT,
    comment,
  };
}

export function recieveComments(comments: Array<Comment>): CommentActionType {
  return {
    type: RECIEVE_COMMENTS,
    comments,
  };
}

export function fetchComments() {
  return (dispatch: Dispatch) => {
    api.fetchComments('/api/comments')
      .then((comments) => {
        dispatch(recieveComments(comments));
      }).catch(error => {
        console.error(error);
      });
  };
}

export function saveComment(comment: Comment) {
  return (dispatch: Dispatch) => {
    dispatch(submitComment(comment));
    api.saveComment('/api/comments', comment)
      .then(() => {
        console.log('save comment');
      }).catch(error => {
        console.error(error);
      });
  };
}
  • public/src/reducers/comments.js
/* @flow */

import * as commentActions from '../actions/comment';
import type { CommentActionType } from '../actions/comment';
import type { Comment } from '../types';

export type CommentsState = {
  comments: Array<Comment>,
}

export default function comment(state: CommentsState = { comments: [] }, action: CommentActionType) {
  switch (action.type) {
    case commentActions.SUBMIT_COMMENT:
      return { comments: state.comments.concat([action.comment]) };
    case commentActions.RECIEVE_COMMENTS:
      return { comments: action.comments };
    default:
      return state;
  }
}

まとめ

  • 導入の障壁などなく、既存のプロジェクトにも部分的に適用することが可能なので試しやすい
    • 基本的には入れて損することはないとい印象
  • 基本的には自分が携わっている範囲では導入に対するデメリットは無いように感じる
    • 参照記事にもあるようにデメリットではなく欠点/問題点という表現が良さそうなのだが、flow-typedに対応ライブラリが少ないというのがあげられると思う。
      • ただ、面倒であればひとまず握りつぶしてしまうことも可能だし、これからどんどん増えていく可能性もあるので導入の障壁とは感じていない

ちなみに、検索エンジンの検索数で「このライブラリが人気」みたいなのは自分は好きじゃないんだけど、flowtypeをgoogle trendで調べたところ注目度は上がっているんだとは思う。

f:id:bokuweb:20160619224133p:plain