エラーの内容
このエラーは、enumメンバーを互換性があるかのように渡したり代入しようとすると発生します:
enum Status {
Active,
Inactive,
Pending,
}
function setStatus(s: Status.Inactive) {
// ...
}
const current = Status.Active;
setStatus(current); // ❌ Type 'Status.Active' is not assignable to type 'Status.Inactive'. ts(2322)
型付き変数間で代入する場合にも同じエラーが発生します:
let a: Status.Active = Status.Active;
let b: Status.Inactive = a; // ❌ ts(2322)
根本原因
TypeScriptは各enumメンバーを独自のリテラル型として扱います。単なる値ではなく、実際に独立した型です。そのため、型レベルではStatus.ActiveとStatus.Inactiveはtrueとfalseと同様に全く異なるものです。
実行時には単なる数値(0と1)です。しかし型チェッカーはそれを気にしません。Status.Inactiveとして型付けされた変数はStatus.Inactiveしか受け付けません——それが全てです。
数値enumにはもう一つ知っておくべき特性があります:プレーンな数値リテラルを受け付けます(let s: Status = 0はコンパイルされます)。しかしenumメンバーは型が異なる場合、互いに代入可能ではありません。
修正1:型をenum全体に広げる
ほとんどの場合、これが修正方法です。関数が任意のStatus値を受け付けるべき場合、パラメータの型をStatus.InactiveではなくStatusとして指定します:
// ❌ 修正前 — 意図せず型が狭すぎる
function setStatus(s: Status.Inactive) { ... }
// ✅ 修正後 — 任意のStatus値を受け付ける
function setStatus(s: Status) { ... }
const current = Status.Active;
setStatus(current); // OK
型が過度に狭くなるのは通常オートコンプリートによるものです——Status.と入力してInactiveを選択すると、気づく前にTypeScriptがリテラル型を固定してしまいます。
修正2:特定メンバーのユニオン型を使う
一部のメンバーだけを受け付け、全てではない場合は?ユニオン型で意図を明示できます:
function handleResolved(s: Status.Active | Status.Inactive) {
console.log(s);
}
handleResolved(Status.Active); // ✅
handleResolved(Status.Inactive); // ✅
handleResolved(Status.Pending); // ❌ 正しく拒否される
修正3:代入前に型ガードを使う
広いStatus値を持っているが、特定のメンバーを期待する箇所に渡す必要がある場合は、まず条件で確認します。TypeScriptの制御フロー解析が残りの処理を行います:
function requireInactive(s: Status.Inactive) {
console.log('Inactive:', s);
}
function process(s: Status) {
if (s === Status.Inactive) {
requireInactive(s); // ✅ TypeScriptはここでsをStatus.Inactiveに絞り込む
}
}
そのifブロック内では、TypeScriptはsがStatus.Inactiveのみであることを認識します。キャストなしで代入が安全になります。
修正4:型アサーション(最終手段)
実行時の値は正しいがTypeScriptがそれを証明できない場合——例えば外部APIから値を取得した後など——にのみ使用してください:
const s = getStatusFromAPI() as Status.Inactive;
requireInactive(s); // ✅ — ただし型安全性を回避している
1〜2箇所以上でas SomeEnum.Memberと書いている場合は、アサーションを増やすのではなく型設計を見直す必要があります。
修正5:enumをやめて文字列リテラルユニオンを使う
モダンなTypeScriptのコードベースの多くはenumを全く使いません。文字列リテラルユニオンはシンプルで、より明確なエラーを生成し、リテラル型の混乱を回避できます:
// 以下の代わりに:
enum Status {
Active = 'active',
Inactive = 'inactive',
Pending = 'pending',
}
// 次を使用:
type Status = 'active' | 'inactive' | 'pending';
function setStatus(s: Status) { ... }
setStatus('active'); // ✅
setStatus('inactive'); // ✅
setStatus('unknown'); // ❌ 正しく拒否される
enumの特有の問題なし、ランタイムオブジェクトなし、インライン展開される値の心配なし。コンパイラが強制する制約を持つ単純な文字列です。
修正6:const enumも同じ問題を持つ
const enumはコンパイル時に値をインライン展開しますが、型ルールは同一です。同じ修正が適用されます——型を広げるかユニオンを使います:
const enum Direction {
Up,
Down,
}
// ❌ 型が狭すぎる
let d: Direction.Up = Direction.Down; // ts(2322)
// ✅ 型を広げる
let d: Direction = Direction.Down; // OK
確認方法
修正を適用したら、実際に解決されたことを確認します:
- 代入箇所の赤い波線がエディタ上ですぐに消えるはずです。
- エディタが見逃している可能性のあるものを検出するため、プロジェクト全体の型チェックを実行します:
npx tsc --noEmit
# 出力なし = エラーなし
- 型ガード(修正3)を使用した場合は、一致するブランチと一致しないブランチの両方を実行する簡単なテストを追加してください——コンパイル時だけでなく実行時にもガードが正しく動作することを確認します。
クイックリファレンス
- パラメータの型が狭すぎる → 型を
Status.XからStatusに変更 - 一部のメンバーのみ必要 →
Status.X | Status.Yを使用 - 条件内での型の絞り込み → 渡す前に
if (s === Status.X)を使用 - enumの複雑さを完全に回避 → 文字列リテラルユニオンに切り替え
予防策
根本原因はほぼ常に同じです:意図がenum全体(Status)であるにもかかわらず、変数やパラメータが誤って特定のenumメンバー(Status.Active)として型付けされてしまうことです。オートコンプリートが通常の原因です——初期値を入力するとTypeScriptがリテラル型を固定してしまいます。
これを防ぐ3つの習慣:
- enum変数には明示的な型アノテーションを書く:TypeScriptに右辺から
Status.Activeを推論させるのではなく、let s: Status = Status.Activeと書く。 - 文字列enumまたは文字列リテラルユニオンを優先する。エラーメッセージがより読みやすく、予期しないエッジケースがはるかに少ない。
tsconfig.jsonでstrictモードを有効にする。定義時点でこれらの不一致を表面化させ、関数呼び出しを3層にわたって波及して混乱を招く前に検出できる。

