Typescriptの配列は型安全ではない
TypeScript
1
要点
- 部分型と破壊的変更を持つメゾットにより配列の型安全性を破壊できる
- readonlyやジェネリクスで対応可能
コード
const foo = (target:{bar:number}[]) => {
target.push({bar:1})
}
const targetArray:{bar:number,piyo:string}[] = []
foo(targetArray)
console.log(targetArray) // [{bar:1}]
type TypeofTargetArray = typeof targetArray // {bar:number,piyo:string}[]
if(targetArray[0]){ // nullチェック
const firstElement = targetArray[0] // {bar:1}
type TypeofFirstElement = typeof firstElement // {bar:number,piyo:string} 型と実際の値が食い違う
const piyo = firstElement.piyo
type TypeofPiyo = typeof piyo // なんとstring
console.log(piyo) // undefined
piyo.at(1) // ランタイムにTypeErrorが発生する
}
このコードはコンパイルを通るがランタイムにぬるぽする
なぜ起こるのか
問題点はfoo の呼び出し時にtargetArray の型が暗黙的に部分型を認め、キャストされていること
読み取り時には部分型{bar:number,piyo:string} で{bar:number}を代用可能とする論理)は正しいが、書き込み時にはこれは正しくない。一見部分型を認めなければいいように見えるが…
なぜ部分型を認めるのか
typescriptはプロトタイプベースであるため、プロトタイプチェーン上に値がある可能性を否定できない。
プロトタイプチェーン上の全ての型を管理する場合、ミュータブルなプロトタイプのオブジェクトを変更するのを追う必要があり、めっちゃ難しい。ゆえにtypescriptCompilerはプロトタイプチェーン上の値の型を追うことはしない。そのため、オブジェクトが持つ値の型を確定させられない。なので、オブジェクトの部分型を認める必要がある。
対策法
Readonly
Readonlyを使用すると、破壊的なメゾットを使用できなくなる。これにより、部分型の「書き込み時にはなりたたない」という問題を解決できる。ただし、破壊的変更が必須な場合には難しい。
ジェネリクス
foo を<T extends Array<{bar:number}>>(targetA:T) として定義すると、pushの呼び出し時に型エラーが発生する。これは、targetArrayがArray<{bar:number,piyo:string}>と推論されるため。型推論コストや複雑さとのトレードオフになる。
まとめ
typescriptは破壊的変更によわよわ