Typescriptで空配列とReturnTypeを使った時の落とし穴
ReturnTypeで空配列が期待通りに評価されない
最近仕事で見つけたTypescriptの型推論が期待通り動かなかった件について紹介します。下記コードは50%の確率でtest2()
のfoo
とbar
がundefined
になり、concat()
の実行箇所でエラーになるコードです。このコードはTypescriptではfoo
とbar
がstring
として評価されていて型エラーとなりません。
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