おまえら禁じられたインデックスアクセスを平気で使ってんじゃねえか!わかってんのか?『ランタイムエラー』が生まれたのは人間がコンパイラオプションに甘えたせいだろうがよ!
TypeScript 4.1 から noUncheckedIndexedAccess オプションが追加されました。このオプションは上記のような配列のアクセスやオブジェクトのプロパティのアクセスをより厳密にします。 具体的には、配列に対するインデックスアクセスやインデックスシグネチャを通じたプロパティのアクセスは常に `undefined` とのユニオン型となります。
まずは次のコードを見てください。一見コンパイルエラーも発生していないので安全なコードに見えます。
/**
* 配列の先頭の要素を取り出す
*/
function head<T>(arr: T[]): T {
return arr[0]
}
/**
* 大文字に変換する
*/
function upperCase(str: string): string {
return str.toUpperCase()
}
const fruits: string[] = ['apple', 'banana', 'lemomn']
const fruit = head(fruits)
// fruit: string
console.log(upperCase(fruit)) // APPLE
しかし、このコードには 1 つ罠が存在します。head
関数に空の配列を渡してみてください。
/**
* 配列の先頭の要素を取り出す
*/
function head<T>(arr: T[]): T {
return arr[0]
}
/**
* 大文字に変換する
*/
function upperCase(str: string): string {
return str.toUpperCase()
}
const fruits: string[] = []
const fruit = head(fruits)
// fruit: string
console.log(upperCase(fruit)) // [ERR]: Cannot read properties of undefined (reading 'toUpperCase')
相変わらずコンパイルエラーは発生していませんね。TypeScript でコンパイルエラーの発生していないコードは基本的に安全と考えられるはずです。しかしながら、このコードを実行すると以下のランタイムエラーは発生してしまいます。
[ERR]: Cannot read properties of undefined (reading 'toUpperCase')
strict
オプションを有効にしているうえに、any
や as
のような危険な機能を使用していないのにも関わらず、なぜコンパイル時にエラーを検出できなかったのでしょうか?
TypeScript はインデックスアクセスに危険性がある
理由は TypeScript のインデックスアクセスには危険性があるためです。前述のコード中の head
関数は arr[0]
のように配列の要素にインデックスでアクセスしています。
JavaScript において配列の要素を参照する際には 配列[インデックス]
でアクセスできます。指定したインデックスが存在する場合問題なくその要素を取得できますが、存在しないインデックスに対してアクセスした場合 undefined
が返されます。
const arr = ['a', 'b', 'c']
const a = arr[0] // a
const b = arr[1] // b
const lol = arr[99] // undefined
しかしながら、TypeScript は型システム上この動作を完全に再現していません。範囲外のインデックスに対してアクセスした場合でも、string
型と推論されます。
const arr = ['a', 'b', 'c']
const a = arr[0]
// const a: string
const b = arr[1]
// const b: string
const lol = arr[99]
// const lol: string
この動作はインデックスシグネチャ を使用した場合も同様です。存在しないプロパティでアクセスしてしまった場合でも undefined
である可能性がないものとして扱われてしまいます。
type Messages = {
[key: string]: string
}
const messages: Messages = {
hello: 'world!',
ping: 'pong',
}
const hello = messages.hello
// const hello: string
const lol = messages.lol
// const log: stirng
messages
オブジェクに lol
プロパティは存在しないので undefined
が返されるはずですが、型システム上は string
型に推論されています。
noUncheckedIndexedAccess オプションで安全性を手に入れる
このような TypeScript の危険性を改善してほしいという要望は多く寄せられていました。そこで TypeScript 4.1 から noUncheckedIndexedAccess オプションが追加されました。このオプションは上記のような配列のアクセスやオブジェクトのプロパティのアクセスをより厳密にします。
具体的には、配列に対するインデックスアクセスやインデックスシグネチャを通じたプロパティのアクセスは常に undefined
とのユニオン型となります。
冒頭であげた例を noUncheckedIndexedAccess
を有効にして再度試してみましょう。TS Playground では上部のタブの TS Config
からコンパイラオプションを設定できます。
/**
* 配列の先頭の要素を取り出す
*/
function head<T>(arr: T[]): T {
// Type 'T | undefined' is not assignable to type 'T'.
// 'T' could be instantiated with an arbitrary type which could be unrelated to 'T | undefined'.
return arr[0]
}
/**
* 大文字に変換する
*/
function upperCase(str: string): string {
return str.toUpperCase()
}
const fruits: string[] = []
const fruit = head(fruits)
console.log(upperCase(fruit))
head
関数内でコンパイルエラーが発生していることがわかります。arr[0]
は undefined
を返す可能性があるので返り値の型である T
に割り当てできないと述べています。
コンパイルエラーを回避するためには head
関数の返り値の型を T | undefined
に変更して、upperCase
関数に渡す前 undefined
でないことをチェックする必要があります。
/**
* 配列の先頭の要素を取り出す
*/
function head<T>(arr: T[]): T | undefined {
return arr[0]
}
/**
* 大文字に変換する
*/
function upperCase(str: string): string {
return str.toUpperCase()
}
const fruits: string[] = []
const fruit = head(fruits)
// const fruit: string | undefined
if (fruit) {
console.log(upperCase(fruit))
}
インデックスシグネチャを通じたプロパティアクセスも同様です。下記の例ではプロパティアクセス時に string | undefind
型になります。
type Messages = {
[key: string]: string
}
const messages: Messages = {
hello: 'world!',
ping: 'pong',
}
const hello = messages.hello
// const hello: string | undefined
const lol = messages.lol
// const log: stirng | undefined
おわりに
noUncheckedIndexedAccess
オプションは非常に厳しいコンパイラオプションであるため、デフォルトでは有効となっていません。とはいえ、TypeScript の新規開発を行う機会がある場合にはこのオプションを有効にすることをおすすめします。
毎回 undefined
でないことを確認する手間は増えますが、安全性を得られることを考えると安い代償でしょう。あるいは、for...of
を使うなどなるべくインデックスアクセスに頼らないコードを意識してみるとよいでしょう。
noUncheckedIndexedAccess
オプションを途中から有効にするのはなかなか手間がかかります。実際に手元のプロパティでためしに noUncheckedIndexedAccess
オプションを有効にしてみるとよいでしょう。きっと大量のコンパイルエラーが報告されるはずです😉。