TypeScript TS2322: Type 'X' is not assignable to type 'Y'
Type is not assignable
Verified against TypeScript docs: Compiler Errors, TypeScript handbook: Type Compatibility, TypeScript 5.5 release notes · Updated May 2026
> quick_fix
TypeScript found a type that doesn't fit the slot it's being assigned to. Read which property differs: the error names both types and points at the divergence. Fix one of three ways: narrow the source type with a type guard, change the target type, or add the missing property/null check.
// Common pattern: missing null check
function Component({ user }: { user: User | null }) {
// TS2322: Type 'string | undefined' is not assignable to type 'string'
return <p>{user.name}</p> // err: user might be null
}
// Fix: narrow with a guard
function Component({ user }: { user: User | null }) {
if (!user) return null
return <p>{user.name}</p> // user is User here
}What causes this error
TS2322 fires when an expression's inferred type isn't assignable to the type expected by the destination - a function parameter, return type, variable annotation, or property assignment. TypeScript checks structural compatibility; the error message names the source and target types and (for object types) the specific property that differs.
How to fix it
- 01
step 1
Read the full error message
TS2322 names both types and (for objects) which property is incompatible. The 'Types of property X are incompatible' suffix is the most actionable line.
Type 'User' is not assignable to type 'AdminUser'. Property 'role' is missing in type 'User' but required in type 'AdminUser'. - 02
step 2
Check for null/undefined in the source type
Most TS2322 errors come from strictNullChecks: the source type includes null or undefined but the target doesn't. Add a guard, use the non-null assertion (sparingly), or change the target type to allow null.
// Bad - target requires non-null const name: string = user?.name // TS2322 if user can be undefined // Fix 1: narrow if (user) { const name: string = user.name } // Fix 2: default const name: string = user?.name ?? '' // Fix 3: type allows undefined const name: string | undefined = user?.name - 03
step 3
Check for missing properties on object types
If you're passing an object literal to a typed slot, TypeScript checks every required property. The error names which one is missing.
type Props = { id: number; name: string; email: string } // TS2322: Property 'email' is missing const p: Props = { id: 1, name: 'a' } // Fix const p: Props = { id: 1, name: 'a', email: '' } - 04
step 4
Check for excess properties
Object literals get strict excess-property checking. If you pass a property the target type doesn't define, TypeScript rejects it. Either remove it, widen the target type, or assign through an intermediate variable to bypass excess-property checking.
type Props = { id: number; name: string } // TS2322: 'extra' does not exist in type 'Props' const p: Props = { id: 1, name: 'a', extra: true } // Fix - widen type or remove type Props = { id: number; name: string; extra?: boolean } - 05
step 5
Check for union vs intersection mismatches
Assigning a union to a stricter slot fails. If a variable is 'string | number' and the target wants only 'string', narrow first with typeof or a type guard.
function getId(): string | number { return 'abc' } // TS2322 const s: string = getId() // Fix - narrow const v = getId() const s: string = typeof v === 'string' ? v : String(v) - 06
step 6
Use a type guard for unknown sources
Data from JSON.parse, fetch, or external APIs is often 'unknown' or 'any'. Validate with zod, custom type predicates, or in/instanceof checks before assigning.
function isUser(x: unknown): x is User { return typeof x === 'object' && x !== null && 'id' in x && 'name' in x } const data: unknown = JSON.parse(raw) if (isUser(data)) { const u: User = data // OK }
Why TS2322 happens at the runtime level
TypeScript's checker performs structural type assignability via the assignableTo function in src/compiler/checker.ts. For object types, it walks every required property of the target and verifies the source has a compatible property; for unions, it checks every member; for intersections, every member. When any check fails, the compiler emits TS2322 with the most specific failure path it can find. Object literals get an additional excess-property check that fires earlier than full assignability. Conditional and mapped types add another layer where the checker may produce 'deeply nested' errors that mention types by their full structural form instead of named aliases.
Common debug mistakes for TS2322
- Forgetting strictNullChecks; assigning user.name when user could be undefined.
- Adding a property to a literal that the target type doesn't allow (excess property check).
- Returning a union type from a function whose signature promises a single type.
- Using 'as' to silence the error instead of fixing the underlying type mismatch.
- Not validating external JSON before treating it as a typed object - the cast lies.
When TS2322 signals a deeper problem
Recurring TS2322 errors in a codebase usually mean type definitions and runtime data have drifted. The architectural fix is to validate at the boundary: zod or valibot at API entry points, schema-first ORM at the database layer, and typed client SDKs for external services. With validation at the edges, internal types are trusted and TS2322 fires only on real bugs. Without it, every fetch and JSON.parse produces a typed value the compiler trusts but reality may not match - meaning TS2322 errors get silenced with 'as' and runtime crashes follow. The deeper signal is that the codebase needs a contract layer where types and runtime agree.
Frequently asked questions
Why does TypeScript reject an object literal that 'should' work?
Object literals get excess-property checking - even if all required properties exist, an extra property triggers TS2322. The reasoning: object literals are usually typos waiting to happen, and the strictness catches misspelled property names. To bypass, assign to an untyped variable first, then to the typed slot. Better: declare the type more permissively, or use Partial<T> if some properties are optional. The strictness is intentional and you should usually fix the literal rather than working around it.
How does TS2322 differ from TS2345?
TS2322 fires on assignments and return types. TS2345 fires on function-argument type mismatches. Both check structural compatibility but the error code distinguishes the location. Practically, the fix patterns are identical: narrow the source, widen the target, or add the missing properties. The two errors are closely related and the same root causes (null in non-null slots, missing properties, union mismatches) trigger both.
When should I use 'as' to silence TS2322?
Almost never. 'as' tells TypeScript 'trust me, this works' and disables the check. If you're wrong, you crash at runtime with no warning. Legitimate uses are narrow: casting unknown to a known shape after runtime validation (zod parse), and asserting a specific subtype after a runtime check the compiler can't follow. Code that uses 'as' frequently usually has a real type problem hidden behind it; fix the types instead. The 'as unknown as Foo' double-cast pattern is a code smell - whatever you're doing probably needs a different design.
What about 'satisfies' vs 'as'?
'satisfies' (TS 4.9+) checks compatibility without widening. const x = { a: 1 } satisfies Record<string, number> verifies x is assignable to the type but x's inferred type stays { a: 1 }. 'as' coerces the type, losing precision. Use satisfies when you want to assert compatibility for safety while keeping the narrower inferred type for downstream type inference. Use 'as' only after runtime validation. They solve different problems and should not be interchanged.