最初に
この記事はflowtype
導入の手順紹介というより、自分の作業ログに近いものです。flowtype
って何?ってところも含めて以下に紹介する記事を見たほうがわかりやすいと思いますので、参照してください。
今回試すにあたって、参考にした記事。
動機
自分の観測範囲内で「flowtypeいいよ!」って話しをよく聴くようになり、試してみることにした。
自分の場合はだが、主な動機としてはReactのpropTypes頑張って書く割に得られる恩恵少ない
というのがあってflowtype
であれば、それを改善しつつ、部分的に適用することができる。
新規プロジェクトで あればTypescript
を採用するなどの選択肢を取ることが可能であるが、既存プロジェクトの場合はそうはいかない。だけど、flowtype
であれば既存のプロジェクトにも型の恩恵を付加することができる。
また上でも紹介している以下の記事内にFLOW SOUNDNESS, NO RUNTIME EXCEPTIONS AS GOAL
とあって、これは導入しない手はないんじゃないか?と思い今に至る。実行時例外のない世界に住んでみたい。
作業ログ
導入
以下で入る。
npm i -D flow-bin babel-plugin-transform-flow-strip-types
現状のバージョンがflow-bin@0.27.0
、babel-plugin-transform-flow-strip-types@6.8.0
、flow-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
の適用を試みてみる。その成果物となるものは以下。
コメント送信部への適用
まずはコメント送信部、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
/* @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
で管理するっぽい。
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-thunk
やredux-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-thunk
やredux-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に適用してみたものは以下のようになる。
Typescript
もflow
もこれまで触ったことがなく、間違っている可能性もあるのでその場合はご指摘ください。
- 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で調べたところ注目度は上がっているんだとは思う。