typescript

おまえら禁じられたインデックスアクセスを平気で使ってんじゃねえか!わかってんのか?『ランタイムエラー』が生まれたのは人間がコンパイラオプションに甘えたせいだろうがよ!

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 オプションを有効にしているうえに、anyas のような危険な機能を使用していないのにも関わらず、なぜコンパイル時にエラーを検出できなかったのでしょうか?

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 からコンパイラオプションを設定できます。

スクリーンショット 2022-06-26 20.39.10

/**
 * 配列の先頭の要素を取り出す
 */
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 オプションを有効にしてみるとよいでしょう。きっと大量のコンパイルエラーが報告されるはずです😉。


Contributors

> GitHub で修正を提案する
この記事をシェアする
はてなブックマークに追加

関連記事