Flowtypeに入門してJavaScriptコードで静的型付けの恩恵をうけるところまで
ハウス・オブ・カードで寝不足の小飼です。
どうなるんですか、あのアレは...
さて、最近個人的にGolang
でアプリケーションを作ったり、Haskell
を勉強したりしています。
いずれも静的型付けにより、実行前に型エラーを検出可能な言語です。
私は普段、動的型付け言語であるJavaScript
を主に書いていますので、初めこそビルド時のエラーにヤキモキしたりしましたが、慣れてくると非常に快適に感じるようになりました。
実行時に検出されるつまらない間違いや、間違った型が渡ってきた時の防御コード(if typeof variable !== 'string' console.warn('型が違う')
のようなコード)が不要になること以外に、アプリケーション固有のデータ構造を型として宣言することで、何をするか
よりも何であるか
に注目した読みやすいコードを書くことが可能になるからです。
React.js
にはPropType
という型システムを匂わせるような仕組みが用意されていますが、実行時の型エラー検出ですし、カスタム型(のようなもの)は独自の型検証関数を用意するようなものでアプリケーションの登場人物を宣言するよな使い方は難しく、上記のようなメリットは受けにくい印象です。
現在JavaScript
界隈で静的型付けを導入するとしたら、以下の2つのライブラリ(トランスコンパイラ)がメジャーな選択肢なのではないでしょうか
(elm
にも静的型付けはあるようですが、メジャーな選択肢として取り得るか?というと...)
- TypeScript
- Flow
Angular2.x~
の基盤となっているTypeScript
も気になるところではありますが、本稿では軽めに始められると噂のFlow
を扱ってみたいと思います。
今回はGithub-API
からユーザのリストを取ってきて、ユーザのサムネイルと名前を描画する、簡単なアプリケーションを作成します。
次にこのアプリケーションへ実際にFlow
を適用することで、静的型付けのメリットが得られるかを検証してみたいと思います。
こんな感じの簡単なものです
動的型付けのサンプル
まずFlow
を導入しない状態のアプリケーションを作成します。
話を簡単にするために、Flux
系の何やかやは使わずに、実直なProps
バケツリレーによりデータを伝播させます。
(実行時の型エラー検出による恩恵は、本稿の主題から外れるためPropType
の宣言もしません)
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import 'whatwg-fetch';
class Root extends Component {
constructor() {
super()
this.state = {
users: []
}
}
componentWillMount() {
fetch('https://api.github.com/users')
.then(r=> r.json())
.then(r=> this.setState({
users: j.map(u=> ({
name: u.login,
img: u.avatar_url,
})),
}))
.catch(e=> console.log(e))
}
render() {
return <Users users={this.state.users}/>
}
}
class Users extends Component {
constructor() {
super()
}
render() {
return <ul>{this.props.users.map((user, i)=> <User key={i} user={user}/>)}</ul>
}
}
class User extends Component {
constructor() {
super()
}
render() {
const { user } = this.props;
return (
<li>
<a href={`https://github.com/${user.name}`}><img src={user.img} alt={user.name} /></a>
{user.name}
</li>
)
}
}
ReactDOM.render(<Root />, document.querySelector('#root'))
特に難しいことはしていないと思います。
Root
コンポーネントにユーザ一覧のコンポーネントを渡して、描画するアプリケーションです。
ここにFlow
による静的型付けを導入していきたいと思います。
導入準備
公式の手順を参考に導入します。
Flow
の特徴としてCLIツールによる型検査
とビルド時の型宣言除去
によって、静的な型検査をしつつアプリケーション実行時には型宣言の影響を持たないことが挙げられます。
これによって、アプリケーションの任意の箇所から段階的に型宣言の恩恵を導入することができる、というコンセプトのようです。
Flow is a static type checker, designed to quickly find errors in JavaScript applications:
Flow is a gradual type system. Any parts of your program that are dynamic in nature can easily bypass the type checker, so you can mix statically typed code with dynamic code.
従って、型検査のためのモジュール
と型宣言除去のためのモジュール
の2種類のモジュールをインストールする必要があります。
# 型検査のためのモジュール(Flowの本体とコマンドラインインターフェース)
npm install --save flowtype flow-bin
# babelによるビルド時に型宣言を除去するためのプラグイン
npm install --save babel-plugin-transform-flow-strip-types
シンプルにbabel
+browserify
でビルドする想定で、依存モジュールはこんな感じにしました。
{
"babel": "^6.5.2",
"babel-plugin-transform-flow-strip-types": "^6.7.0",
"babel-plugin-transform-react-jsx": "^6.7.4",
"babel-plugin-transform-class-properties": "^6.6.0",
"babel-preset-es2015": "^6.6.0",
"babelify": "^7.2.0",
"browserify": "^13.0.0",
"flow-bin": "^0.22.1",
"flowtype": "^1.0.0",
"react": "^0.14.7",
"react-dom": "^0.14.7",
"whatwg-fetch": "^0.11.0"
}
.babelrc
の設定はこんな感じです
{
"presets": [
"es2015"
],
"plugins": [
"transform-react-jsx",
"transform-class-properties",
"transform-flow-strip-types"
]
}
※ ES Class Fieldsでprops/state
の型検査を行いたいので、[babel-plugin-transform-class-properties](https://www.npmjs.com/package/babel-plugin-transform-class-properties)
をインストールしています。
各種スクリプトはこんな感じで、npm run flow
で型検査を、npm run browserify
でビルドをそれぞれ実行します。
{
"flow": "$(npm bin)/flow",
"browserify": "$(npm bin)/browserify app.js -t babelify -o bundle.js",
"build": "npm run flow && npm run browserify"
}
Flow
用の設定ファイル.flowconfig
を、公式ドキュメントの手順を参考に生成して、型検査の対象ファイルに@flow
をコメントアウトして記述します。
デフォルトだとnode_modules
も検査しに行ってしまうので、設定ファイルの[ignore]
に書いておいた方がいいでしょう。
[ignore]
bundle.js
.*/node_modules/fbjs/*
[include]
[libs]
declare.js
[options]
esproposal.class_instance_fields=enable
esproposal.class_static_fields=enable
独自型のデザイン
準備が整ったので、サンプルアプリケーションを静的型付けっぽくデザインしなおしてみます。
このアプリケーションで登場する主なデータ型にはユーザの名前と画像を持ったデータ
とユーザの配列
があるようですので、こんな感じで宣言してみます。
// ユーザの名前と画像を持ったデータ型
type TypeUser = {
name: string;
img: string;
}
// ユーザの配列
type TypeUsers = Array<TypeUser>;
次に実際に型検査をしてみます。
TypeUser型はUserコンポーネントのprops
として使われますので、Userコンポーネントのprops
プロパティに型指定をします。
class User extends Component {
props: {
user: TypeUser;
};
static defaultProps = {
user: { name: '', img: '' },
};
/*... */
}
これでUserコンポーネントに渡せるデータは、TypeUser型だけになったはずです。
npm run flow
で型検査をしてみると、No errors!
と表示されるはずです。
確認のため、間違ったデータ型を渡してみましょう。
TypeUser型
が持っているはずのname/img
プロパティをいずれも持っていないオブジェクトを渡してみます
class Users extends Component {
/*... */
render() {
// return <ul>{this.props.users.map((user, i)=> <User key={i} id={i} user={user}/>)}</ul>
return <ul>{[{noname: '', noimg: ''}].map((user, i)=> <User key={i} user={user}/>)}</ul>
}
}
app.js:57
57: user: TypeUser;
^^^^^^^^ property `img`. Property not found in
53: render = ()=> (<ul>{[{noname: '', noimg: ''}].map((u, i)=> <User key={i} user={u}/>)}</ul>);
^ object literal
app.js:57
57: user: TypeUser;
^^^^^^^^ property `name`. Property not found in
53: render = ()=> (<ul>{[{noname: '', noimg: ''}].map((u, i)=> <User key={i} user={u}/>)}</ul>);
^ object literal
Found 2 errors
あるべきプロパティが無いことを警告してくれました!
ブラウザで実行する前に型エラーを検出してくれるので、安心して開発ができそうです
同じように、コンポーネントの各所で必要な型を書いていき、最終形はこんな感じになりました。
// @flow
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import 'whatwg-fetch';
type TypeUser = {
name: string;
img: string;
}
type UsersType = Array<TypeUser>;
class Root extends Component {
state: {
users: UsersType
};
constructor() {
super()
this.state = {
users: [],
}
}
componentWillMount() {
fetch('https://api.github.com/users')
.then(r=> r.json())
.then(j=> {
this.setState({
users: j.map((u): TypeUser => ({
name: u.login,
img: u.avatar_url,
})),
})
})
.catch(e=> console.log(e))
}
render() {
return <Users users={this.state.users}/>
}
}
class Users extends Component {
props: {
users: UsersType;
};
static defaultProps = {
users: []
};
constructor() {
super()
}
render() {
return <ul>{this.props.users.map((user, i)=> <User key={i} user={user}/>)}</ul>
}
}
class User extends Component {
props: {
user: TypeUser;
};
static defaultProps = {
user: { name: '', img: '' },
};
constructor() {
super()
}
render() {
const { user } = this.props;
return (
<li>
<a href={`https://github.com/${user.name}`}><img src={user.img} alt={user.name} /></a>
{user.name}
</li>
)
}
}
ReactDOM.render(<Root />, document.querySelector('#root'))
この状態でnpm run browserify
コマンドを打つと、transform-flow-strip-types
が型宣言を除去してからbundle
ファイルを生成できます。
静的型付けの恩恵を受けつつ、実行コードには影響が無いのが良いですね。
まとめ
静的型付け最高という思いからFlow
を試してみましたがいかがだったでしょうか。
既存のアプリケーションの任意の箇所から、lint
ツールのように小さく気軽に使い始められるのが良いですね。
個人的には、独自の型を作るところから発想した方が合理的な構造のアプリケーションを設計しやすいように感じているので、業務コードでも積極的に取り入れて行きたいと思っています。
ダメそうだったらすぐ離れられる軽さもうれしいところです。
以上、参考になれば幸いです
今回ご紹介したような、合理的な構造のビジネス・アプリケーション開発を御希望の企業様は、是非MMMにご相談下さいませ!
参考
- Flow | A static type checker for JavaScript
- Flowtypeを使ってReactComponentで型安全を手に入れる
- 型なき世界のためのflowtype入門