「AWS無料相談会」をオンラインで開催中

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 Fieldsprops/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にご相談下さいませ!

参考