読者です 読者をやめる 読者になる 読者になる

TypeScriptのvoid型との付き合い方 ~ あるいはundefinedを使うな

programming TypeScript

TypeScriptは後付けの型とはいえ信頼境界の内側に入れば基本的にboolean型を宣言した変数に真理値以外が入ることはない。 nullundefinedおよびany型(細かいことを言えば{}型とObject型も)さえ使わなければ型を信じることができる。

と思ってたのだが言語設計的にはそうでもないようだ。

前述のようなバッドプラクティスを避けても、void型については以下のように他のすべての型が入る可能性がある。

function f(): void {
}
function g(): number {
    return 1;
}
function dispatch(callback: () => void): void {
    return callback();
}

var a: void = dispatch(f); // undefined
var b: void = dispatch(g); // 1

これはvalidなコードだが変数bにはvoid型を宣言しているにもかかわらず数値が代入される。 void型の変数など通常使わないし使うべきではないが適切なプログラミングを行ってもこの性質が問題となることがある。 戻り値とその真理値的な使用だ。

JavaScriptには戻り値がundefinedか否かで動作が変わるものが少なくない。JQueryのeachメソッドがfalseを返すと中断するのはよく知られていることだろう。

$().each(_ => !dispatch(g)); // trueを返したかった

Promiseのメソッドチェインでもふとした拍子に問題となることがありえる。 また、常に偽と評価されるfalsyな条件式としてハック的に使用されることもある。

() => dispatch(g) || dispatch(f); // 両方呼びたかった

これはまったく型安全ではない。 戻り値の型のvoidは呼び出し側から見ればanyも同然だ。 このvoid型とどう付き合っていけばいいのだろうか。

思うに、void型の唯一の値であるundefinedはTypeScriptでは未定義という状態を設定するためにのみvoid演算子または空により未定義を逐次生成して使用するべきであり、値として使うべきではない。 そしてvoid型とは値がundefinedである安全や信頼を確保するために使うものではない。

未定義の状態の設定とは関数でreturnを使いながら使わなかったときと同じ結果(戻り値)を作ったり、変数やプロパティの値をやむをえず明示に初期化するような操作を意図している。

他の型は前述のバッドプラクティスを避けて自分で安全を確保すれば、たとえばboolean型であればtrueまたはfalseのいずれかである安全と信頼を確保できる。しかしvoid型は関数の呼び出し過程で他のあらゆる型の値が混入する可能性があり、これはanyやnullなどの特定のキーワードの禁止といった比較的簡単な方法では回避できない。void型は他の型とは意味も性質もまったく異なるのだ。

TypeScriptにおけるvoid型の意味の1つは、値の破棄の要求である。 関数の戻り値に限らず、すべてのvoid型の値は再利用の余地なく即座に破棄しなければならない。 他の型のように型に任せるのではなく、型のためにスタイルの変更が必要となる規約のようなものとして付き合わなければならない。 戻り値の型がvoidの関数はいっそすべてvoid演算子を噛ませたほうがいいかもしれない。

() => void dispatch(g);
function _(): boolean {
    void dispatch(g);
    return true;
}

TypeScriptにおけるvoid型のもう1つの意味は、ダウンキャスト的な柔軟性の採用である。 すべての値はundefinedに変換できるため戻り値の型のみ異なるすべての関数は戻り値がvoid型である関数に変換できる。 このような変換を想定していると考えれば戻り値におけるすべての型がvoid型に代入できることも、まあわからなくもない。 この手法はハック的だが戻り値の型にvoidを指定することは珍しくないので大量のコードの中から目視で区別して排除するのは不可能に近い。

function f(): boolean {
    return false;
}
function g(): number {
    return 1;
}
function dispatch(callback: () => void): void {
    callback();
}

dispatch(f);
dispatch(g);

こう書かれていると安全かつ少しはわかりやすい。

function dispatch(callback: () => void): void {
    return void callback();
}

こちらは型のみだが、類似の柔軟性をパラメータにおいて見ることができる。 オプションのパラメータを持つ関数は、よりオプションが少ない関数に代入できる。

function f(a?, b?): boolean {
    return false;
}
function g(a?): number {
    return 1;
}
function dispatch(callback: () => void): void {
    callback();
}

dispatch(f);
dispatch(g);

他にも有意な解釈や利用法があるかもしれないが自分がすぐに思い浮かんだのはこれくらいだ。 あとは他にまかせる。

https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md#325-the-void-type