みなさんassert
を使っていますか!!
function foo(str) {
console.assert(typeof str === 'string');
console.assert(str.length > 0);
// 何かの処理
str.charAt(0);
}
assert
を使って事前条件を書いて開発し、unassertを使ってプロダクションにリリースするときに削除スタイルは最近良く見かけるようになりましたね。
動的型付けなJavaScriptで安全なコードを書けるようになるassert
最高!!と言いたいところですが、実行時にしかチェックができないんですよね。
そこで、せっかくflowtypeで静的な型付けを行っているので、ここは事前にチェックできるように型で解決を行いたいと思います。
1. 型アノテーションを付与する
まずは先のコードに型アノテーションを付与して一つ目のassert
を削除します。
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以上のチェック処理を行っているクラスを作って、それを受け取るように変更したいと思います。
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
である型を作ります。
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
に変換する処理を端折ったので、そこを追加します。
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を実装する話があるので参考にしてください。
5. 検査エラーを返すようにする
ここで完成としても良かったのですが、検査エラーでnull
を返すのはイケてないのでエラーを適切に返せるようにします。
真面目に作るならEitherやValidationを作っても良いのですが、今回は簡単にPromiseで表現することにします。
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のための機能になりますので、ちょっと用途が違います。
今回の内容みたいなものでも%checks
構文が使えると凄い楽になるので新機能として欲しいですね!!でも、型の世界の話しだけで済まなくなるので、たぶん実装されることはないんでしょうけど。
追記(2017/07/10)
なんか遊んでたら汎用的なPredicateType
ができました。
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
はいけてないなーというのと、もっと短くしたいなーというのでもう少し作りこめみたい気持ちでいっぱいです。