The Error
This one shows up when you try to pass or assign enum members as if they're interchangeable:
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)
Same error when assigning between typed variables:
let a: Status.Active = Status.Active;
let b: Status.Inactive = a; // โ ts(2322)
Root Cause
TypeScript treats each enum member as its own literal type โ not just a value, but an actual distinct type. So Status.Active and Status.Inactive are as different as true and false at the type level.
At runtime? They're just numbers (0 and 1). But the type checker doesn't care. A variable typed as Status.Inactive will only accept Status.Inactive โ full stop.
Numeric enums have one more quirk worth knowing: they accept plain number literals (so let s: Status = 0 compiles). But enum members still aren't assignable to each other when their types differ.
Fix 1: Widen the Type to the Full Enum
Nine times out of ten, this is the fix. If the function should accept any Status value, type the parameter as Status, not Status.Inactive:
// โ Before โ accidentally too narrow
function setStatus(s: Status.Inactive) { ... }
// โ
After โ accepts any Status value
function setStatus(s: Status) { ... }
const current = Status.Active;
setStatus(current); // OK
The over-narrow type usually creeps in via autocomplete โ you type Status., pick Inactive, and TypeScript locks in the literal type before you realize it.
Fix 2: Use a Union of Specific Members
Need to accept some members but not all? A union type makes the intent explicit:
function handleResolved(s: Status.Active | Status.Inactive) {
console.log(s);
}
handleResolved(Status.Active); // โ
handleResolved(Status.Inactive); // โ
handleResolved(Status.Pending); // โ Correctly rejected
Fix 3: Use Type Guards Before Assigning
When you already have a broad Status value but need to pass it somewhere that expects a specific member, use a condition first. TypeScript's control flow analysis will do the rest:
function requireInactive(s: Status.Inactive) {
console.log('Inactive:', s);
}
function process(s: Status) {
if (s === Status.Inactive) {
requireInactive(s); // โ
TypeScript narrows s to Status.Inactive here
}
}
Inside that if block, TypeScript knows s can only be Status.Inactive. The assignment becomes safe without any casting.
Fix 4: Type Assertion (Last Resort)
Only reach for this when you're certain the runtime value is correct but TypeScript can't prove it โ for example, after fetching a value from an external API:
const s = getStatusFromAPI() as Status.Inactive;
requireInactive(s); // โ
โ but you're bypassing type safety
If you find yourself writing as SomeEnum.Member in more than one or two places, the type design needs rethinking โ not more assertions.
Fix 5: Drop the Enum, Use String Literal Unions
A lot of modern TypeScript codebases skip enums entirely. String literal unions are simpler, produce clearer errors, and avoid the whole literal-type confusion:
// Instead of:
enum Status {
Active = 'active',
Inactive = 'inactive',
Pending = 'pending',
}
// Use:
type Status = 'active' | 'inactive' | 'pending';
function setStatus(s: Status) { ... }
setStatus('active'); // โ
setStatus('inactive'); // โ
setStatus('unknown'); // โ Correctly rejected
No enum quirks, no runtime object, no inlined values to worry about. Just strings with compiler-enforced constraints.
Fix 6: const Enum Has the Same Problem
const enum inlines values at compile time, but the type rules are identical. Same fix applies โ widen the type or use a union:
const enum Direction {
Up,
Down,
}
// โ Too narrow
let d: Direction.Up = Direction.Down; // ts(2322)
// โ
Widened
let d: Direction = Direction.Down; // OK
Verification
Once you've applied a fix, confirm it's actually resolved:
- The red squiggly under the assignment should disappear immediately in your editor.
- Run a full project type-check to catch anything the editor might have missed:
npx tsc --noEmit
# No output = no errors
- If you used a type guard (Fix 3), add a quick test that exercises both the matching and non-matching branches โ confirm the guard behaves correctly at runtime, not just at compile time.
Quick Reference
- Parameter too narrow โ change type from
Status.XtoStatus - Need subset of members โ use
Status.X | Status.Y - Narrowing inside a condition โ use
if (s === Status.X)before passing - Avoiding enum complexity altogether โ switch to string literal unions
Prevention
The root cause is almost always the same: a variable or parameter accidentally typed as a specific enum member (Status.Active) when the intent was the full enum (Status). Autocomplete is the usual culprit โ you fill in an initial value and TypeScript locks in the literal type.
Three habits that prevent this:
- Write explicit type annotations for enum variables:
let s: Status = Status.Activerather than letting TypeScript inferStatus.Activefrom the right-hand side. - Prefer string enums or string literal unions. The error messages are more readable and there are far fewer surprising edge cases.
- Turn on
strictmode intsconfig.json. It surfaces these mismatches at the point of definition, before they ripple through three layers of function calls and become confusing.

