assert便利だ。使っといて損ないな。
でもまだTypeScriptでassert薦める記事ないなーと思ったので記事を書くことにしました。
要約
- TypeScript 3.7 で追加された Assertion Function が @type/node の assert に対応した (2020年4月頃) (プルリク)
- assert でtype guard が効くようになり便利になった。
- babel-plugin-unassert などを使うことで本番ではassert を取り除くことができるので本番影響はない (他の言語ではあたりまえな機能)
- 気軽に契約プログラミングの考えを一部取り入れることで、実装者の意図が分かる & バグの特定のしやすさに繋がり開発体験が上がる
assert使ってる?
TypeScripterの皆さんはassertを普段使っているでしょうか?使ってないorテストコードにしか使ってない方も多そうです。
本来のassertは表明(Assertion)を実現するためのものです。テストコード以外にも使えます。
コード例
Node.jsの機能であるassertはWebpackなどの環境であればpolyfillがあり使うことができます。
このような感じに使います。
import assert from 'assert'
doSomething() {
assert(foo)
console.log(foo.text) // type guard が効いている
}
assert を使うことでfoo
が truthyであることを表明しています。
表明というのは簡単に言えば、実装者はfoo
が truthy であることを前提にしている 、すなわち truthy でなければコードにバグが含まれている という意図を表すということです。
もしfooがfalsyであった場合、assert はAssertionError を起こします。これによって開発中はバグの特定がしやすくなります。
そしてこのassert関数はTypeScript3.7 で組み込まれたAssertion Functionsに対応しています。 ( 対応プルリク https://github.com/DefinitelyTyped/DefinitelyTyped/pull/42786 )
@types/node
のassert関数の型定義↓
function assert(value: any, message?: string | Error): asserts value;
これによって assert(foo)
の呼び出しをしたスコープでは foo がtruthy と見做されるtype guard が効くようになっています!
もう少し実践的なコードを見てみます。
import assert from 'assert'
type Foo = {
text: string
}
// 副作用があるなどの理由でconstructor内で呼びたくない関数という仮定
function createFoo(param: number): Foo | null {
return param > 0 ? { text: 'foooooo' } : null
}
export class SomeClass {
private foo?: Foo | null
constructor(private param: number) {}
public start(): void {
this.foo = createFoo(this.param)
if (!this.foo) {
throw Error('invalid param')
}
this.doSomething()
}
private doSomething() {
// if (!this.foo) return // これをやりがち
assert(this.foo) // これを使おう。this.fooは truthy であることを表明する。
console.log(this.foo.text) // type guardされているのでエラーは出ない
}
}
const some1 = new SomeClass(1)
some1.start() // 'foooooo' が出力される
const some2 = new SomeClass(0)
some2.start() // Error('invalid param') が起こる。(正しいコードではassertはfalseになりえない)
少しコードが長くなりましたがどうってことないでしょう。
doSomething
という プライベート関数(プライベート関数であることがわりと重要) では this.foo
は必ず non nullable ということを 実装者は分かっています。なぜなら呼び出し元の start
関数では doSomething
を呼び出す前に this.foo
が null になる場合に例外を投げてバリデーションをしているからです。
ですが、TypeScript の型推論ではそういったことは把握していなく、this.foo
はnullableだと思っているので普通に console.log(this.foo.text)
を呼び出そうとしたら怒られます。
この場合、今までだったらtype guardをするためにdoSomething
関数の最初に早期リターンを書くでしょう。しかしこれには少し問題があります。それはこれは「本当にreturn したい文」なのか「本来は実行されることはありあえないが、type guard したいから仕方なく書いてる」だけなのかが見分けられない点です。今回は後者です。
その点、assert の何がうれしいかと言ったら、this.foo が nullになることはありあえないと表明することができ、読む人はその意図を汲み取れます。そしてassert に引っかかるということはコードにバグがあるということであり、問題の発生箇所が特定しやすくなります。
何でもかんでもassert使うということではない
if (!this.foo) return
のような文を絶対書くなということではありません。
処理の流れとして、nullの場合return するのが正しい挙動である場合、もちろん必要になります。
逆にそういう場合にassertを使ったら当たり前ですがエラーを吐きます。assertは本番では削除される(後述)のでAssertionError をキャッチすることはありえません。
本番環境でのassertの削除
ここまで読んで来た方は、「assertと例外は何が違うの?」とか「本番でAssertionErrorで落ちる危険性高くない?」と思った人もいるでしょう。
ここがassert の肝と言っても良いのですが、他の言語だと、assertは言語機能としてあり、実行・コンパイルオプションでassertを解除できる機能が備わっているのが普通です。つまり本番ではassertは文ごと削除されるという前提でassertを書くので、安心してassertを使っているのです。
JavaScriptでそれを実現するにはどうするかというと、 unassert というプロジェクトを活用します。
Babel であれば、 babel-plugin-unassert が使って本番環境の場合assertを取り除きます。 t_wada さんに感謝。
Babel + TypeScript の構成がよくわからないという方は以下のサンプルを参考にすると良さそうです。
https://github.com/microsoft/TypeScript-Babel-Starter
power-assert
普通のassert よりも power-assert を使うことでアサーションのエラーメッセージがいい感じに表示されるのでオススメです。 普通のassertとAPIが変わらない点がとても良いです。
null チェック以外の活用
今回のサンプルコードではif文によるnullチェックの代わりにassert をすると便利ということでしたが、それ以外のシーンでもassertの使い所はたくさんあります。
契約プログラミングを真面目に勉強すると(僕は6時間勉強してみました)、事前条件、事後条件、(おまけに不変条件) というのがあり、今回の例は事前条件にあたるものです。事後条件のassertではassert文自体をがんばって記述する必要があり、なかなか手軽には使えないと思いました。まずは事前条件の記述(特にnullチェック)から怖くない契約プログラミングの入門をしてみてはいかがでしょうか(私はします)。
まとめ
assertの利点は以下です。
- 実装者の意図が明確になる
- バグの特定が容易になる
unassertで本番への影響はないので欠点なし。
そして
- TypeScript のassert関数の型推論が良くなっている。
- assertを使って契約プログラミング入門しよう
- まずはif文nullチェックの代わりにassertを使ってみるのが手軽かつ効果的
というお話でした。
機は熟した。assert を使っていこう。
ご意見あればコメント、Twitterなどでよろしくおねがいします。