要約:クイックフィックス
switch 文や if/else ブロックで**網羅的チェック (exhaustive checking)**を強制しようとした際に、このエラーに遭遇したのではないでしょうか。コンパイラが default ケースに値が漏れる可能性があると判断して警告を出しています。
修正するには、次の2点を確認してください。
- 入力を絞り込む: 変数は、汎用的な
string型ではなく、特定のユニオン型(例:'success' | 'error')である必要があります。 - 漏れをなくす: ユニオン型に含まれるすべての可能性のある値が、
never型への代入に到達する前のcaseで処理されている必要があります。
// 成功パターンの例
type Status = 'active' | 'inactive';
function handleStatus(status: Status) {
switch (status) {
case 'active':
return 1;
case 'inactive':
return 0;
default:
// すべての 'Status' のパターンが上記で処理されている場合にのみコンパイルが通る
const _exhaustiveCheck: never = status;
return _exhaustiveCheck;
}
}
想定されるシナリオ
請求サービスののリファクタリングをしていると想像してください。あなたは意識の高いエンジニアとして、網羅的チェックを実装することにしました。このパターンは「仕掛け線」として機能します。もし来月、チームメイトが新しい支払い方法を追加したとしても、新しいロジックが処理されるまでコンパイラがエラーを出してくれます。これは素晴らしいセーフティネットです。
しかし、そこで問題が発生します。役立つヒントが表示される代わりに、コンパイラは次のような難解なエラーを吐き出します。
Type 'string' is not assignable to type 'never'. (TS2322)
これは直感に反するように感じられます。ケースをすべて書いたはずなのに、TypeScript は変数が依然として汎用的な string であると言い張り、never への代入を拒否するのです。
なぜこのエラーが発生するのか?
TypeScript において、never は「ありえない」型です。default ブロックで変数に never を代入することは、「この行にコードが到達することは論理的に不可能である」と大胆に主張していることになります。
TS2322 エラーは、TypeScript がロジックに穴を見つけたときに発生します。主な原因は次の3つです。
1. 入力が広すぎる
TypeScript は慎重です。関数の引数が特定のリテラルのユニオン型ではなく、汎用的な string として型定義されている場合、コンパイラは あらゆる テキストが関数に入ってくる可能性があると想定します。たとえ 'A' と 'B' を処理していたとしても、入力が 'Z' である可能性があります。'Z' は default ブロックに落ちるため、そこでの型は never ではなく string になってしまいます。
2. 「ケースの不足」という罠
これは更新後に頻繁に起こります。ユニオン型を 2 つから 3 つに増やした(例:'success' | 'failed' に 'pending' を追加した)ものの、switch 文を更新しなかった場合です。TypeScript は 'pending' がまだ未処理であることを検知します。その値を default ブロックに渡そうとするため、エラーがトリガーされます。
3. 型の拡大 (Type Widening)
複雑なロジックや不適切なキャストによって、特定のリテラル型が string へと「拡大」してしまうことがあります。具体性が失われると、コンパイラは default ブロックに到達不能であることを保証できなくなります。
解決方法
方法1:入力型を厳密にする
まず、データがどこから来ているかを確認してください。API レスポンスやフォーム入力からのデータである場合、型定義が緩い可能性があります。型ガード (Type Guard) や特定のユニオン定義を使用して、型を絞り込んでください。
// ❌ 問題:'status' が単なる string 型になっている
function process(status: string) {
switch (status) {
case 'open': return;
default:
const _ex: never = status; // エラー:string は never に割り当てられない
}
}
// ✅ 解決策:厳密なユニオン型を使用する
type TicketStatus = 'open' | 'closed';
function process(status: TicketStatus) {
switch (status) {
case 'open': return;
case 'closed': return;
default:
const _ex: never = status; // エラーなし!
}
}
方法2:'assertNever' ヘルパーを使用する
手動での代入も問題ありませんが、ユーティリティ関数を使う方がスマートです。これにより、誰かが as any を使ってコンパイラをバイパスした場合でも、明確な実行時エラーを提供できます。また、他の開発者にとっても意図が明確になります。
function assertNever(x: never): never {
throw new Error("Unexpected value: " + x);
}
type Plan = 'basic' | 'pro' | 'enterprise';
function getPrice(plan: Plan) {
switch (plan) {
case 'basic': return 10;
case 'pro': return 20;
case 'enterprise': return 50;
default:
return assertNever(plan);
// 後で 'premium' プランを追加した場合、この行で即座に TS2345 エラーが発生する
}
}
方法3:列挙型 (Enum) に注意する
列挙型を扱うときは、Enum 型そのものに対して switch を行っているか確認してください。Enum を生の文字列と比較すると、コンパイラが型を正しく絞り込む能力を損なうことがあります。
enum UserRole { Admin, Editor }
function checkAccess(role: UserRole) {
switch (role) {
case UserRole.Admin: return true;
case UserRole.Editor: return true;
default:
const _check: never = role; // 期待通りに動作する
}
}
確認手順
- 変数を検査する: VS Code で
defaultブロック内の変数の上にマウスをホバーさせます。ツールチップに(parameter) status: "pending"と表示される場合は、単にそのケースの処理を忘れているだけです。status: stringと表示される場合は、入力型が緩すぎます。 - キャストを探す: コードベース内で
as stringを検索してください。過度なキャストは、網羅的チェックが依存しているユニオン型を剥ぎ取ってしまうことがよくあります。 - 穴をテストする: TypeScript が警告している値に対して、一時的な
caseを追加してみてください。エラーが消えれば、ロジックに分岐が不足していたことが確認できます。
まとめ
TS2322 エラーはコンパイラのバグではありません。コンパイラは、あなたが依頼した通りの仕事を正確にこなしているだけです。つまり、値が漏れる可能性のあるロジックの穴を見つけたのです。入力を絞り込むか、ユニオン型のすべてのバリアントを明示的に処理することで解決しましょう。

