🎄

noUncheckedIndexedAccess / TypeScript一人カレンダー

2024/12/19に公開

こんにちは、クレスウェア株式会社の奥野賢太郎 (@okunokentaro) です。本記事はTypeScript 一人 Advent Calendar 2024の11日目です。昨日は『実例 hooksTestingTools()』を紹介しました。

配列アクセス時のundefinedリスクを検出するオプション

TypeScript 4.1から--noUncheckedIndexedAccessというコンパイラオプションが追加されています。これは、配列やタプルに添字アクセスした際、その要素が存在しない可能性を型で表現するオプションです。strictを有効にしていても、このオプションはデフォルトで無効なため、別途明示的に有効化する必要があります。

筆者は初期からこのオプションを使っていますが、2年前のカレンダーではうっかり紹介を忘れていました。とはいえ、このオプションは実務において非常に有用なツールです。なぜなら、存在しない配列要素へのアクセス時にランタイムでundefinedが返る可能性を、コンパイル時に明示的に指摘してくれるからです。

無効時と有効時の挙動を比較

noUncheckedIndexedAccess無効時の例を見てみましょう。

const arr = [0, 1, 2];
const v = arr[3];
//    ^? number
console.log(v); // 型はnumberだが、実行時にはundefined

この状況では、arr[3]は実際にundefinedを返しますが、型システムはnumberであるとして扱います。実行時エラーを引き起こしかねない、危険な状況です。

一方、noUncheckedIndexedAccess有効時は以下のように挙動が変わります。

const arr = [0, 1, 2];
const v = arr[3];
//    ^? number | undefined
console.log(v); // undefinedの可能性ありとコンパイル時に判明

コンパイラはarr[3]number | undefinedであると推論し、strictオプションと組み合わせていれば、undefinedの可能性を考慮せず利用しようとするときにエラーを起こします。これにより、潜在的なundefined混入の危険性をコンパイル時点であぶり出すことができます。

undefinedチェックの手間を惜しまない

2年前のカレンダーで紹介したassertExists()との組み合わせも有効です。

const arr = [0, 1, 2];
const v = arr[3];
assertExists(v);
console.log(v); // number型であることが確定する

ただ、こういった関数の利用が必要なことについて「毎回undefinedを考慮して行数が増えるのは面倒」と感じる人もいるかもしれません。とはいえ、実務上は想定外のundefinedが混入して問題が発生し、それを後から調査する手間のほうがよっぽど面倒だと思っています。コンパイラに警告されることで事前にエラー箇所を特定し、安全なコードを書くことができるのであれば、毎回検証する手間は掛けるに値するでしょう。

また、テストコードなどであれば、?.オプショナルチェーン演算子)を使ってundefinedを簡潔にハンドリングできます。以下はVitestのようなテストコードでよく使われる例です。この例ではNext.jsredirect()関数を適切に使用できているかを検証しています。

test("redirect先のパスが正しい", () => {
  expect(redirectSpy.mock.calls[0]?.[0]).toEqual("/path/to");
});

noUncheckedIndexedAccessが有効だとredirectSpy.mock.calls[0][0]と書いた場合に、[0][0]の箇所でエラーとなります。その結果、このように?.を使う習慣がつき、予期せぬ実行時エラーを防ぐことができます。

as const も併用する

とはいえ、テストのモックデータを記述するときなどで「目の前にlength 2であることが明らかな配列があるのに、undefinedのリスクを気にするのは冗長」という状況もあるでしょう。

type Item = {
  id: string;
  name: string;
}

function getName(item: Item): string {
  // ...
}

const items = [
  { id: 'itemId0', name: 'itemName0' },
  { id: 'itemId1', name: 'itemName1' },
];

getName(items[0]); // モックの配列からモックデータを取り出す時はシンプルに書きたい

ここでは?.も使えないためチェックが煩雑に感じるかもしれません。そんなときには、as constを指定するのが便利です。

const items = [
  { id: 'itemId0', name: 'itemName0' },
  { id: 'itemId1', name: 'itemName1' },
] as const;

getName(items[0]); // 確実に取り出せるとわかるのでチェック不要
getName(items[2]); // 存在しないことが明らかであるため、コンパイルエラー

この指定を追加することでlength 2の配列であることが確定し要素長が変化しないことが保証されるため、存在するindexであればエラーを出さず、存在しないindexのときにエラーを出すようになります。

テストのモックデータなどで、中身が変化しない固定の配列リテラルを作成する機会は多いと思いますので、こういった例ではas constを積極的に付与していきましょう。

今すぐオプションの有効化を検討しよう

strictオプションの有効化だけではnoUncheckedIndexedAccessは有効になりません。もし現在、noUncheckedIndexedAccessを有効にしていないのであれば、これを機に導入を検討してみるとよいでしょう。新規案件で一から開発環境を構築する際も、ここはうっかり忘れがちになるので気をつけたいところです。

明日は『satisfies』

本日は「noUncheckedIndexedAccess」を紹介しました。それではまた。

Discussion