前にこのブログの記事に書いたように、TypeScript 1.4以降(ターゲットがES5の場合は1.5以降)では let/const による変数宣言ができるようになった。let は書き換え可能な変数で、const は書き換え不可能な変数だ。let 宣言と const 宣言の登場によって、var 宣言は用済みになったと言っていいだろう。
let も const もスコープに関する規則は同じなので、書き換えない変数に対して let と const のどちらを使っても違いはないはず。そう思って、個人的に書いているコードでは文字数を重視して常に let の方を使っていた。
しかし、 TypeScript 2.0 で導入された control flow based type analysis により、「書き換えない変数に対して let と const のどちらを使っても違いはない」とは言ってられなくなった。
次のようなコードを考える。変数 a のスコープを明確にするためにIIFEで囲っている。コンパイルオプションには --strictNullChecks
をつける。
declare function f(): (() => void) | null; (() => { let a = f(); if (a !== null) { a(); } })();
このコードにおいて、 a の呼び出しのタイミングを遅らせてみる。
declare function f(): (() => void) | null; (() => { let a = f(); if (a !== null) { setTimeout(() => { a(); }, 0); } })();
すると、 --strictNullChecks
付きでコンパイルした場合に
test2.ts(5,28): error TS2531: Object is possibly 'null'.
というエラーメッセージが出る。
このコードを読んだプログラマーは、「変数 a は変更されないので setTimeout のコールバック中でも a は非 null である」と判断できる。しかし、 TypeScript のコンパイラーからすれば、 setTimeout を呼び出してからコールバック関数が呼び出されるまでの間に変数 a が書き換えられる可能性があるため、 a が null である可能性を排除できない。
仮に、コンパイル時に、関数内から参照されるそれぞれの変数に対して、代入が1箇所でもあるかどうかを確かるような処理を入れれば、先の例の変数 a は変更されないと判断できて、コールバックの中でも非 null であると判断できるだろう。しかし、全ての変数についてそのようなチェックを行うのは、(想像だが)コンパイルが遅くなったりというようなコストの問題があるので、そのようなチェックは行なっていないのだろう。(少なくとも TypeScript 2.0 の時点では)
一方、変数 a の宣言を let から const に変えれば、コンパイラーから見ても変数 a は変更されないと容易に断定できる(変数 a に代入する箇所があればその時点でコンパイルエラーになる)ので、コールバック関数の中でも変数 a は非 null だと判断してくれる。
declare function f(): (() => void) | null; (() => { const a = f(); if (a !== null) { setTimeout(() => { a(); }, 0); // コンパイルが通る } })();
そんなわけで、TypeScript 2.0 では「2文字多くタイプしてでも、 const 宣言を使った方がいい理由」ができた。