0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TypeScriptでコールバック内の代入が型に反映されない理由と対処法

Last updated at Posted at 2025-12-12

はじめに

コールバック内で変数に値を代入した後に型エラーが出る問題についての解説と対処法をまとめました

問題の再現

let found: string | null = null;

['apple', 'banana', 'orange'].forEach((item) => {
  if (item === 'banana') {
    found = item;  // ここで代入しているのに
  }
});

if (!found) return;

// エラー: 'found' is of type 'never'
console.log(found.toUpperCase());

なぜこうなるのか

この問題は 2つの要因の組み合わせ で発生します。

要因1: リテラル値の代入で型が絞り込まれる

let found: string | null = null;
// ↑ この時点で found の型は「null」(リテラル型)に絞り込まれる
// 宣言時の string | null ではなく、具体的な値 null として認識

TypeScript は = null というリテラル値の代入を見て、「この変数は今 null だ」と型を絞り込みます。

要因2: コールバック内の代入は追跡されない

TypeScript の型推論はコールバックの実行を追跡しません。

['apple', 'banana', 'orange'].forEach((item) => {
  if (item === 'banana') {
    found = item;  // ← TypeScript はこの代入を型推論に反映しない
  }
});

// forEach 後も found の型は「null」のまま

結果: if 文で never になる

if (!found) return;

// TypeScript の型絞り込み:
// 「found の型は null」
// 「if (!found) return で、null なら return する」
// 「ここに到達した = found は null ではない」
// 「null から null を除外 → 何も残らない → never」

null 型から null を除外すると何が残るか?」→「何も残らない」→「never

補足: 変数を宣言だけした場合

初期値を与えずに宣言だけした場合はどうでしょうか?

let found: string | null;  // 初期値なし

['apple', 'banana', 'orange'].forEach((item) => {
  if (item === 'banana') {
    found = item;
  }
});

if (!found) return;  // エラー: Variable 'found' is used before being assigned.

この場合は never ではなく、「代入前に変数が使用された」 という別のエラーになります。コールバック内の代入が追跡されないため、TypeScript は「一度も代入されていない」と判断します。

対処法

方法1: 初期化時に型アサーションを使う

初期化時に型を広げることで、絞り込みを防ぎます。

let found: string | null = null as string | null;

['apple', 'banana', 'orange'].forEach((item) => {
  if (item === 'banana') {
    found = item;
  }
});

if (!found) return;
console.log(found.toUpperCase());  // OK

メリット: 変数宣言の1箇所を変えるだけで解決

方法2: 使用時に型アサーションを使う

let found: string | null = null;

['apple', 'banana', 'orange'].forEach((item) => {
  if (item === 'banana') {
    found = item;
  }
});

if (!found) return;

(found as string).toUpperCase();  // OK

メリット: 既存コードへの影響が少ない

デメリット: 使用箇所ごとにアサーションが必要

方法3: Non-null アサーション(!

let found: string | null = null;

['apple', 'banana', 'orange'].forEach((item) => {
  if (item === 'banana') {
    found = item;
  }
});

if (!found) return;

found!.toUpperCase();  // OK

メリット: 最も簡潔

デメリット: 実行時エラーのリスクがある

方法4: 型注釈を明示して再代入

let found: string | null = null;

['apple', 'banana', 'orange'].forEach((item) => {
  if (item === 'banana') {
    found = item;
  }
});

// 型注釈で string | null に広げる
const result: string | null = found;

if (!result) return;
console.log(result.toUpperCase());  // OK

メリット: 型安全を維持できる

デメリット: 変数が増える

まとめ

Typescriptは奥が深いですね
もっといい方法あれば教えてください。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?