flowtypeで既存の型に述語を付与してassertを削除しよう

みなさん`assert`を使っていますか！！

```js
function foo(str) {
  console.assert(typeof str === 'string');
  console.assert(str.length > 0);

  // 何かの処理
  str.charAt(0);
}
```

`assert`を使って事前条件を書いて開発し、[unassert](https://github.com/unassert-js/unassert)を使ってプロダクションにリリースするときに削除スタイルは最近良く見かけるようになりましたね。
動的型付けなJavaScriptで安全なコードを書けるようになる`assert`最高！！と言いたいところですが、実行時にしかチェックができないんですよね。
そこで、せっかくflowtypeで静的な型付けを行っているので、ここは事前にチェックできるように型で解決を行いたいと思います。

# 1. 型アノテーションを付与する

まずは先のコードに型アノテーションを付与して一つ目の`assert`を削除します。

```js
function foo(str: string): void {
  console.assert(str.length > 0);

  // 何かの処理
  str.charAt(0);
}

foo(1); // Error
```

これで`foo`に`number`や`boolean`を呼び出してもflowtypeの型チェックでエラーが出るようになりましたね！

# 2. 専用のクラスを作成する

次は`console.assert(str.length > 0)`を型で表現したいと思います。
とは言っても型で文字列の長さが1以上あることを表現することは難しいです。（型レベル自然数なら出来るとは思うけどflowtypeで作るのは大変だと思う）

そこで、まずは文字列の長さが1以上のチェック処理を行っているクラスを作って、それを受け取るように変更したいと思います。

```js
class NonEmptyString {
  str: string;
  constructor(str: string) {
    console.assert(str.length > 0);
    
    this.str = str;
  }

  get(): string {
    return this.str;
  }
}

function foo(str: NonEmptyString) {
  // 何かの処理
  str.get().charAt(0);
}

foo(new NonEmptyString('abc'));
```

これで、`foo`関数の中から`assert`が消えシンプルになりましたね。

# 3. Intersection Typeに変更する

`foo`関数自体はだいぶシンプルになりましたが、`NonEmptyString`では`string`と同じように扱えないためちょっと面倒臭いですね。
`NonEmptyString`で`String`を継承しても良いのですが、flowtype的に`String`と`string`では別物なため細々と不具合が出てきます。

そこで、Intersection Typeを使って`NonEmptyString`であり、`string`である型を作ります。

```js
declare class NonEmptyStringIdentifier {}
type NonEmptyString = string & NonEmptyStringIdentifier;

function foo(str: NonEmptyString) {
  str.charAt(0);
}

declare var str: NonEmptyString;
foo(str);
```

これで、`foo`関数内で通常の文字列として扱いつつ、`assert`を削除することができました。

# 4. NonEmptyStringに変換する関数を作成する

先の例では`declare var`で`string`を`NonEmptyString`に変換する処理を端折ったので、そこを追加します。

```js
declare class NonEmptyStringIdentifier {}
type NonEmptyString = string & NonEmptyStringIdentifier;

interface Predicate<S, A> {
  apply(s: S): ?A
}
const NES: Predicate<string, NonEmptyString> = {
  apply: (s) => {
    return s.length > 0 ? ((s:any):NonEmptyString) : null;
  }
}

function foo(str: NonEmptyString) {
  str.charAt(0);
}

let str = NES.apply('abc');
if (str) {
  foo(str);
}
```

もう少し真面目に作るならflowtypeでLens/Prismを実装する話があるので参考にしてください。

* [Introduction to optics: lenses and prisms](https://medium.com/@gcanti/introduction-to-optics-lenses-and-prisms-3230e73bfcfe)

# 5. 検査エラーを返すようにする

ここで完成としても良かったのですが、検査エラーで`null`を返すのはイケてないのでエラーを適切に返せるようにします。
真面目に作るならEitherやValidationを作っても良いのですが、今回は簡単にPromiseで表現することにします。

```js
declare class NonEmptyStringIdentifier {}
type NonEmptyString = string & NonEmptyStringIdentifier;

interface Predicate<S, A> {
  apply(s: S): Promise<A>
}
const NES: Predicate<string, NonEmptyString> = {
  apply: s =>
    (s.length > 0) ? Promise.resolve(((s:any):NonEmptyString))
                   : Promise.reject(new Error('string is empty.'))
}

function foo(str: NonEmptyString) {
  str.charAt(0);
}

async () => {
  foo(await NES.apply('abc'));
};
```

Errorも専用のものを用意した方がより適切だとは思いますが、今回は面倒なので端折っています。

# 6. 完成

という訳でこれで完成です！
今回の例だと`assert`と違って`unassert`でプロダクションにリリースしたときに削除できないため、関数呼び出しのオーバーヘッドなどパフォーマンス面が気になる方がいると思います。
しかし、実際にアプリケーションに導入する場合はフォームや、APIのレスポンスのJSONをオブジェクトに変換するときなど、外部から入力された値に対しては検査を行っているはずなので、そのタイミングで検査済みの値として述語を付与した`NonEmptyString`のような型を返してやればそこまでパフォーマンスの劣化は起きないはずです。
（もし検査してなかったら、そもそも`assert`を消しちゃいけないよねという話でもありますけど）

逆にどういったときに`assert`を使うべきかと言えば、述語を付与した型を作るのが面倒臭いときぐらいなのかなと思います。
もし、こういったときは`assert`でないと難しいというものがあれば教えてください。

# おわりに

そういえばflowtypeには`$Pred<T>`がありますが、あれは[Type Refinements](https://flow.org/en/docs/lang/refinements/)のための機能になりますので、ちょっと用途が違います。
今回の内容みたいなものでも`%checks`構文が使えると凄い楽になるので新機能として欲しいですね！！でも、型の世界の話しだけで済まなくなるので、たぶん実装されることはないんでしょうけど。

# 追記（2017/07/10）

なんか遊んでたら汎用的な`PredicateType`ができました。

```js
interface P<S, A: S> {
  apply(f: (s: S) => boolean): {
    refine(s: S): Promise<A>;
  }
}

const Predicate: P<*, *> = {
  apply: (f) => ({
    refine: (s) =>
      f(s) ? Promise.resolve((s:any))
           : Promise.reject(`Invalid Predicate: '${s}' does not satisfy '${f.toString()}'`)
  })
};

interface NonEmptyStringInterface {};
type NonEmptyString = string & NonEmptyStringInterface;

const NES = (Predicate: P<string, NonEmptyString>).apply(_ => _.length > 0);

(async () => {
  let value = await NES.refine('abc');
  value;
})();
```

どうせ値をAny Typeに変更しないといけないので、だったら型は関数の戻り型で適切に変換してやればいいやんの精神です。

そして、やっぱり`Promise`はいけてないなーというのと、もっと短くしたいなーというのでもう少し作りこめみたい気持ちでいっぱいです。