はじめに
コールバック内で変数に値を代入した後に型エラーが出る問題についての解説と対処法をまとめました
問題の再現
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は奥が深いですね
もっといい方法あれば教えてください。