Typescriptで空配列とReturnTypeを使った時の落とし穴

2025/02/21に公開

ReturnTypeで空配列が期待通りに評価されない

最近仕事で見つけたTypescriptの型推論が期待通り動かなかった件について紹介します。下記コードは50%の確率でtest2()foobarundefinedになり、concat()の実行箇所でエラーになるコードです。このコードはTypescriptではfoobarstringとして評価されていて型エラーとなりません。
test2()の引数はtest()の戻り値としてReturnTypeを使って定義しているのになぜこのようなことが起きるのでしょうか。

function test() {
    if (Math.random() < 0.5){
        return []
    }

    return ['foo', 'bar'] as const
}

function test2(input: ReturnType<typeof test>) {
    const [first, second] = input
    console.log(first.concat(second))
}

// [ERR]: Cannot read properties of undefined (reading 'concat')
// または
// [LOG]: "foobar"
test2(test())

空配列の型はnever[]として扱われる

空配列の型はnever[]として扱われるようです。このためtest()の戻り値の型は never[] | readonly ["foo", "bar"] となります。この型の1番目の要素は never | "foo"inputの2番目の要素はnever | "bar"となり、これらはそれぞれ "foo", "bar"のリテラル型になります。これはnever型が「決して発生しない値の型」を表現するための特殊な型であり、never型と他の型のUnionは必然的にもう片方の型として扱われるためです。

こうした結果、test()の戻り値の1番目の要素も2番目の要素もともにundefinedを含まない型となり、実行時にundefinedとなる可能性があるにもかかわらず、型チェックではエラーとして検出されません。

回避方法

以下のように空配列にもas constをつけます。as constをつけることで空配列の型がreadonly []になります。これにより、test()の戻り値の型は readonly [] | readonly ["foo", "bar"]になり、1番目の要素の型はundefined | "foo"inputの2番目の要素は`undefined | "bar"になるため、undefinedの考慮を促す型エラーが発生しなくなります。

function test() {
    if (Math.random() < 0.5){
        return [] as const
    }
    return ['foo', 'bar'] as const
}

おわりに

asを使って無理矢理型を上書きしたわけでもないのにundefinedの情報が抜け落ちて型エラーが出ないことがあるのが面白いなと思ったので記事にしました。Typescriptの型は過信しないようにしていきたいと思います。

コミューン株式会社

Discussion