Flow による型のある世界入門

みなさんこんにちは!DeNA Games Osaka 技術部のさいです。今回は JavaScript に静的な型付けを導入する Flow (公式サイト) についてご紹介させていただこうと思います。

 

 

 

 

Flow とは?

JavaScript に静的な型付けを導入するチェッカーです(日本では flowtype という呼称の方が馴染みがあるかもしれません)。関数の引数や返り値、あるいは変数宣言に型定義(Flow ではアノテーションと呼びます)を記述することで、型に違反した値を代入する構文を事前にエラーチェックしたり、あるいは型推論により、アノテーションがなくても、型を推測して、事前にエラーチェックすることができます。

 

Flow の事前準備

まずは下記コマンドで、Babel, ES2015, Flow の環境を揃えます。

mkdir src dist
npm init --yes
npm install -g browserify
npm install --save-dev babelify@7.2.0 babel-preset-es2015
npm install --save-dev flow-bin babel-plugin-transform-flow-strip-types
echo '{"presets": ["es2015"],"plugins": ["babel-plugin-transform-flow-strip-types"]}' > .babelrc 

flow-binがチェッカー本体で、babel-plugin-transform-flow-strip-typesが、Babel によるトランスパイル時にコードに記載されたアノテーションを除去してくれるものです。

package.json に flow checkerを追加します。

"scripts": {
    "flow": "$(npm bin)/flow",
  },

.flowconfig も作成して、node_modules 配下はチェックしないように設定します。

[ignore]
.*/node_modules/.*

[include]

[libs]

[options]

 

 

Flow を使ってみる

それでは事前準備ができたので、公式のサンプル5つを一通り試していってみましょう。

https://flowtype.org/docs/five-simple-examples.html

なお、公式のサンプルは、下記レポジトリよりダウンロードすることもできます。

https://github.com/facebook/flow/tree/master/examples


それでは「1. Hello Flow!」より始めていきます。 src/app.js を作成して以下を記述します。

// src/app.js
// @flow

(function() { 
function foo(x) {
  return x * 10;
}

foo('Hello, world!');
});

”// @flow” と記述することで、Flow チェッカーのチェック対象となります。

コードの中身ですが、foo 関数は引数で受け取った x を 10倍にする関数です。ただ、この関数を実際に呼び出す際に、引数にテキストを渡してしまっていますね。これを Flow でチェックするとどうなるでしょう。

下記のコマンドでチェックしてみます。

$ npm run flow

5:   return x * 10;
            ^ string. The operand of an arithmetic operation must be a number.

特にまだアノテーションは追加していませんが、Flow の型推論により、x に string 型の値が入っているコードになっていたため、エラーになりました。

下記のように foo 関数の引数を 10 に変更するとチェックは無事に通ります。

// src/app.js
// @flow

(function() { 
function foo(x) {
  return x * 10;
}

// This is fine, because we're passing a number now
foo(10);
});

このように、数値を引数に取るような関数に、文字列という別の型が代入されるコードを書くと、Flow のチェックにより事前に発見することができます。

それではサンプル2つ目の「2. Adding type annotations」をやってみましょう。

// src/app.js
// @flow

(function() { 
function foo(x: string, y: number): string {
  return x.length * y;
}

foo('Hello', 42);
});

このサンプルコードでは、foo 関数の引数の x に string 型を、 引数 y に number 型を、そして返り値に string 型を指定するアノテーションが存在します。ただしこのサンプルコードでは、返り値を、x の文字数と y の乗算、つまり数値を返しています。これを Flow でチェックしてみるとどうなるか確認してみましょう。

$ npm run flow
5:   return x.length * y;
            ^ number. This type is incompatible with the expected return type of
4: function foo(x: string, y: number): string {
                                       ^ string

型推論により、返り値として数値を返しているにも関わらず、アノテーションによる返り値の型定義が string 型なので、エラーになりました。下記のようにアノテーションを number 型の返り値で定義することでエラーはなくなります。

// src/app.js
// @flow

(function() { 
// Changing the return type to number fixes the error
function foo(x: string, y: number): number {
  return x.length * y;
}

foo('Hello', 42);
}); 

このように、意図していない型の引数を受け取ったり、あるいは意図していない型の返り値を返してしまうことによるバグを未然に防ぐことができます。

それでは次のサンプルコードを見ていきましょう。「3. Nullable types」は null が入る場合の型チェックです。

// src/app.js
// @flow

(function() { 
function length(x) {
  return x.length;
}

var total = length('Hello') + length(null);
})

length 関数の引数に文字あるいは null を指定しています。Flow によるチェックを実行してみるとどうなるでしょうか。

$ npm run flow

7:   return x.length;
              ^ property `length`. Property cannot be accessed on possibly null value
7:   return x.length;
            ^ null

null が入る可能性があるため、エラーになってしまいました。このコードを修正するとどうなるでしょうか。

// src/app.js
// @flow

(function() { 
function length(x) {
  if (x !== null) {
    return x.length;
  } else {
    return 0;
  }
}

var total = length('Hello') + length(null);
});

上記のように、length 関数内で if 分岐し、null である場合とnullでない場合のことを考慮した実装にすると、Flow がチェックの際に、このコードは null が代入されることを考慮出来ているコードだと判断してくれて、エラーを回避できます。

また上記のコードではアノテーションを記述せずに、Flow の型推論にチェックを任せていましたが、null であることを許容する型定義をする場合は以下のようになります。

(function() { 
function length(x : ?string) : number {
  if (x != null) {
    return x.length;
  } else {
    return 0;
  }
}

var total = length('Hello') + length(null);
});

x の型に ?string を定義することで、string あるいは null が入ると型定義しています。

Flow では、string (null は入らない) と ?string (null が入ることもある) のように、null が入る型と入らない型を明確に区別しています(null安全と一般的に呼ばれます)。これにより、意図しない null が代入されていることによるエラーを未然に防ぐことができます。

アノテーションでは配列の型定義をすることもできます。「4. Arrays」のサンプルを見てみましょう。

// src/app.js
// @flow

function total(numbers: Array<number>) {
  var result = 0;
  for (var i = 0; i < numbers.length; i++) {
    result += numbers[i];
  }
  return result;
}

total([1, 2, 3, 'Hello']);

total の関数の引数 numbers の型に Array<number> を定義しています。一方、total 関数の呼び出し時に、number と string を混在させた配列を渡しています。これを Flow で型チェックするとどうなるでしょうか。

13: total([1, 2, 3, 'Hello']);
                    ^ string. This type is incompatible with
5: function total(numbers: Array<number>) {
                                 ^ number

上記のように配列に string 型の値が入っているため、エラーになりました。

下記のように数値だけで構成された配列を渡すことでエラーを回避できます。

// src/app.js
// @flow

function total(numbers: Array<number>) {
  var result = 0;
  for (var i = 0; i < numbers.length; i++) {
    result += numbers[i];
  }
  return result;
}

total([1, 2, 3, 4]);

引数に複数の型を受け取る場合のサンプルを見てみます。「5. Dynamic code」を見ていきましょう。

// src/app.js
// @flow

(function() { 
function foo(x) {
  return x.length;
}

var res = foo('Hello') + foo(42);
});

上記のコードですが、foo 関数を呼び出す際の引数に文字列を指定している場合と、数値を指定している場合があるため、下記のようにエラーになります。

5:   return x.length;
              ^ property `length`. Property not found in
5:   return x.length;
            ^ Number

下記のように引数の型によって処理を分けることで、エラーを回避できます。

// src/app.js
// @flow

(function() { 
function foo(x) {
  if (typeof x === 'string') {
    return x.length;
  } else {
    return x;
  }
}

var res = foo('Hello') + foo(42);
});

最後に、上記の5つのサンプルには記載されていませんが、独自な型定義を利用することもできます。

// src/app.js
// @flow

// 独自の型
type Comment = {
  author: string;
  text: string;
};

const comment: Comment = {
        author: "sai",
        text: "hello, world!",
};

// 引数 comment の型に Comment を使う
function create_text (comment : Comment) : string {
        return comment.author + " says " + comment.text;
}

console.log(create_text(comment));

以上、5+1つのサンプルをご紹介させていただきました。これらのコードに記載されたアノテーションは JavaScript 本来の文法ではないため、そのまま実行しようとするとエラーになります。

$ browserify ./src/app.js -t babelify --outfile ./dist/app.js

とビルドすることで、アノテーションは削除されて実行できる形式になった ./dist/app.js が出力されます。

 

なぜ静的型付けが必要か?

静的型付けとは、変数や、関数の引数や返り値の値の型が、コンパイル時にわかる性質のことです。一方、JavaScript は動的型付けです。変数の中身や、サブルーチンの引数や返り値は実行されるまでわかりません。動的型付けがスクリプト言語の強みであったはずです。なぜわざわざ静的型付けを導入するのでしょう?

静的型付けの導入により、アノテーションの記述分だけ、記述しなければならないコードは増え、そして Flow のチェックによるトランスパイルプロセスが一つ増える分、開発時のコストは増えます。一方、保守していくことを考えると、静的型付けにより以下のような恩恵が受けられます。

  1. コードの読みやすさの向上

  2. リファクタのしやすさの向上

  3. エラーの早期発見

こうしたメリットは、システムが複雑で、かつ長期間保守され続けるシステムで、そしてミッションクリティカルなシステムであるほど有用です。そうしたシステムにおいて、静的な型付けは、システムに堅牢さをもたらします。

 

TypeScript で良いのでは?

JavaScript に静的型付けを導入する場合、Flow の他に、Microsoft の TypeScript も存在します。

Flow と TypeScript を比較すると、TypeScript は AltJS の一種であり、言語そのものが通常の JavaScript と異なります。そのため新規プロジェクトにおいて採用する選択肢を取ることは可能ですが、既存プロジェクトに採用するのは困難な道のりとなります。

一方、Flow はあくまで型チェックツールであり、既存プロジェクトの必要な箇所にアノテーションを追加すれば、その部分だけ型の恩恵を受けることが可能です。

既存のシステムの任意の箇所から、lintツールのように小さく気軽に使い始められる点が、Flow が TypeScript より勝る点でしょう。

 

まとめ

というわけで、JavaScript に静的な型付けを導入する Flow についてご紹介させていただきました。静的型付けをシステムに導入することにより、より堅牢なシステムを作ることができること、そして、Flow は、既存のシステムに対しても部分的に導入しやすいメリットがあります。ぜひ皆様も一度、試してみてください。

 

さいの入門シリーズはコチラ