Article

🛡️ TypeScriptのnoUncheckedIndexedAccessで配列・辞書のundefined事故を防ぐ

typescript, javascript, backend

個人開発しているもの

日々の開発で得た知識を発信しながら、「あったら便利」「作ってみたい」と思ったアイデアを形にしています。

これまでに、グループ旅行アプリ Trip Supporter、スコア管理サービス PointPia、リアルタイム共有サービス JustNow などを開発しました。

さらに、刺繍やオリジナルキャラクターを中心としたアパレルブランド MAD TOMMY WITH... も運営しています。

開発したサービスや技術記事は chicchi.work で紹介しています。

Record<string, number> で在庫を管理すると、存在しない商品コードを読んでも、TypeScriptが number と判断する場合があります。しかし実際のJavaScriptは、存在しないキーを読むと undefined を返します。配列の範囲外アクセスでも同じ問題が起こります。

今回は tsconfig.json の noUncheckedIndexedAccess を使って、「存在しないかもしれない値」をコンパイル時に検出し、業務APIに安全な分岐を追加する方法を紹介します。

2026年11月6日にTypeScript公式TSConfigリファレンスとTypeScript 4.1リリースノートを確認しました。以下のサンプルはTypeScript 5.8.3 / Node.js 22.16.0で型検査・実行を確認しています。この環境のバージョンを最新版と主張するものではありません。

1. strict だけでは検出できないケース

次のような型は、インデックスシグネチャ(キーが任意の文字列で、値は数値とみなす型の宣言)を持ちます。

const prices: Record<string, number> = { shirt: 3900 };

const price = prices["unknown"]; // 通常は number と推論され得る
console.log(price.toFixed(0));   // 実行すると TypeError

strict を有効にしていても、通常このアクセスは未登録キーの危険性を型に反映しません。noUncheckedIndexedAccess を有効にすると、price の型は number | undefined になり、toFixed() の前に確認が必要です。

undefined は「値が定義されていない」状態です。型に追加するだけで、実行時の値を書き換える機能ではありません。

2. tsconfig.jsonで有効にする

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true
  }
}
  • strict:TypeScriptの厳格な型検査をまとめて有効にします。
  • noUncheckedIndexedAccess:辞書や配列の添字アクセスに、値が存在しない可能性を反映します。

公式リリースノートによると、noUncheckedIndexedAccess は strict に含まれず、個別に有効化するオプションです。既存プロジェクトへ追加すると新たな型エラーが出るため、いきなり !(非nullアサーション)で抑えるのではなく、値がない場合の仕様を決めて修正します。

3. 実務例:在庫数の「0」と「未登録」を区別する

0 は「登録済みだが在庫なし」、undefined は「商品コードが存在しない」です。両者を混同して if (!current) と書くと、在庫ゼロと未登録を区別できません。

以下を stock-demo.ts として保存してください。値をローカル変数に読み取った後、分岐してから更新するため、存在確認を曖昧にしません。

type ReserveResult =
  | { ok: true; remaining: number }
  | { ok: false; reason: "missing" | "not-enough" | "invalid-quantity" };

const stockBySku: Record<string, number> = {
  shirt: 4,
  patch: 0,
};

function reserve(sku: string, quantity: number): ReserveResult {
  if (!Number.isInteger(quantity) || quantity <= 0) {
    return { ok: false, reason: "invalid-quantity" };
  }

  const current = stockBySku[sku]; // number | undefined
  if (current === undefined) {
    return { ok: false, reason: "missing" };
  }
  if (current < quantity) {
    return { ok: false, reason: "not-enough" };
  }

  const remaining = current - quantity;
  stockBySku[sku] = remaining;
  return { ok: true, remaining };
}

function firstTag(tags: string[]): string | undefined {
  return tags[0];
}

function check(condition: boolean, message: string): void {
  if (!condition) throw new Error(message);
}

const a = reserve("shirt", 2);
check(a.ok && a.remaining === 2, "正常な予約");

const b = reserve("patch", 1);
check(!b.ok && b.reason === "not-enough", "在庫ゼロ");

const c = reserve("unknown", 1);
check(!c.ok && c.reason === "missing", "キーなし");

const d = reserve("shirt", 0);
check(!d.ok && d.reason === "invalid-quantity", "数量不正");

check(firstTag([]) === undefined, "空配列");
console.log("5 checks passed");

ReserveResult は判別可能なユニオン型です。ok が true のときは remaining、false のときは reason を使う契約にしているため、呼び出し側でも成功・失敗を取り違えにくくなります。

動作確認コマンド

TypeScriptを開発依存に追加済みのプロジェクトなら、次のように実行します。

npx tsc --strict --noUncheckedIndexedAccess --target ES2022 --module commonjs --outDir dist stock-demo.ts
node dist/stock-demo.js
  • npx tsc:プロジェクト内のTypeScriptコンパイラーを起動します。
  • --strict:厳格な型チェックを行います。
  • --noUncheckedIndexedAccess:未確認のキー・添字から得る値に undefined の可能性を加えます。
  • --target ES2022:出力するJavaScriptの言語レベルを指定します。
  • --module commonjs:Node.jsで使うCommonJS形式のモジュールを出力します。
  • --outDir dist:生成したJavaScriptを dist ディレクトリへ保存します。
  • node dist/stock-demo.js:出力されたプログラムを実行します。

期待する出力は 5 checks passed です。package.json が "type": "module" のプロジェクトでは、module 設定と出力ファイル形式をプロジェクトに合わせて変更してください。

4. 配列にも影響する:for と for...of

noUncheckedIndexedAccess を有効にすると、string[] の tags[0] も string | undefined として扱われます。空配列かもしれないためです。

function label(tags: string[]): string {
  const first = tags[0];
  return first === undefined ? "(タグなし)" : first.toUpperCase();
}

添字が不要なら、次のように要素そのものを繰り返すのが簡潔です。

function printTags(tags: string[]): void {
  for (const tag of tags) {
    console.log(tag.toUpperCase()); // tagはstring
  }
}

一方、数値の i を使う for (let i = 0; i < tags.length; i++) のループは、範囲チェックを書いていても tags[i] に undefined の可能性が残ることがあります。添字が必要なら読み取り後に明示的に確認する方が安全です。

5. ハマりどころと使い分け

書き方 何が起こるか 向いているケース
value === undefined 数値の 0 などを維持しながら欠損を判定 在庫・金額・件数
value ?? defaultValue null / undefined のときだけ既定値にする 欠損時に代替値を使ってよい設定
value?.toString() 値がないとき処理をスキップ 値の省略が許容される表示
value! 型チェック上の警告を抑える 実行時に存在が別途保証される限定的な箇所

注意: 商品の価格が見つからないときに prices[sku] ?? 0 とする設計は危険です。「価格不明」を「無料」に変えてしまいます。型エラーが出たら「存在しないときに何をするか」を先に決めましょう。

また、noUncheckedIndexedAccess は 存在が型で明示されているプロパティを一律に省略可能にするわけではありません。例えば type Config = { apiUrl: string } の config.apiUrl は引き続き string です。一方で、Record<string, string> の任意キー参照は確認が必要になります。

導入の判断

新規プロジェクトや、動的なキーを多用するバックエンドAPIでは、最初から有効にすると欠損処理を設計しやすくなります。既存の大規模コードでは、型エラーが多数出る可能性があるため、どのキー・配列アクセスで実際に欠損が起こり得るかを調べて段階的に改修するのが現実的です。

ただし、このオプションは実行時の入力検証や同時更新制御の代わりにはなりません。今回の在庫サンプルは単一プロセスの型と分岐の例であり、実際の複数ユーザーによる購入処理ではデータベースのトランザクションや条件付き更新などを別途検討してください。

まとめ

noUncheckedIndexedAccess は、辞書の未登録キーや配列の範囲外アクセスを「実際には undefined があり得る」という型で表現するオプションです。strict とは別に有効化し、0 と欠損の区別、エラー応答の設計、ループの書き方を見直すと、データ取り違えを防ぐ助けになります。

参考資料(公式)