Quick answer
A value's type doesn't match where you're assigning it. Read the error top-down to the innermost line to find the exact field, then make the types match — fix the value, correct the declared type, or narrow it. The most common variant is string | undefined not assignable to string under strictNullChecks — guard it or use ?? ''.
The exact error string
let count: number = "5";
// error TS2322: Type 'string' is not assignable to type 'number'.
interface User { id: number; name: string; }
const u: User = { id: 1, name: 42 };
// error TS2322: Type 'number' is not assignable to type 'string'. (on name)
How to read a TS2322 error (find the real cause)
The first line names the two top-level types, but the actual mismatch is usually deeper. Read down to the innermost Types of property 'x' are incompatible / Type 'A' is not assignable to type 'B' line — that pinpoints the field and the exact types to change. In your editor, hover the red underline to see the same nested breakdown.
const users: User[] = [{ id: "42", name: "Ada" }];
// error TS2322: Type '{ id: string; name: string; }[]' is not assignable to type 'User[]'.
// Type '{ id: string; name: string; }' is not assignable to type 'User'.
// Types of property 'id' are incompatible.
// Type 'string' is not assignable to type 'number'. <-- the real cause
Ignore the noisy outer types and jump to the last line: id is a string where User.id is a number. That single field is what you fix.
Fix 1: make the value and the type agree
Decide which side is correct — the value or the declared type — and fix that one:
let count: number = 5; // value corrected
let id: string = "5"; // or the type corrected to match the value
Fix 2: strictNullChecks — handle null/undefined
The single most common TS2322 is a possibly-undefined value assigned to a non-optional type. Narrow it before assigning:
function f(name?: string) {
const s: string = name; // ❌ string | undefined not assignable to string
const s1: string = name ?? ""; // ✅ default
if (name !== undefined) {
const s2: string = name; // ✅ narrowed
}
}
Fix 3: widen the target with a union (when both are valid)
If the target legitimately holds more than one type, declare a union — but only when both values truly belong there:
let id: string | number;
id = 5; // ✅
id = "abc"; // ✅
Fix 4: assertions — last resort
A type assertion silences the check without changing the value, so use it only when you genuinely know more than the compiler (e.g. right after validating external data):
const el = document.getElementById("app") as HTMLDivElement; // you verified it's a div
const n = (value as unknown) as number; // double assertion — a red flag, avoid
Worked example: TS2322 from a mistyped API payload
This is the most common real-world trigger on a JSON-heavy app: a hand-written interface that has drifted from what the API actually returns. The API sends an id as a quoted string, your interface says number, and the assignment fails:
interface User { id: number; name: string; }
// The API actually returns: { "id": "42", "name": "Ada" }
const res = await fetch("/api/user");
const user: User = await res.json();
// error TS2322: Type 'string' is not assignable to type 'number'. (on id)
The fix is accurate types, not a cast. Generate the interface from a real response with JSON to TypeScript — it types id as string to match the data — then convert deliberately if your app needs a number:
interface ApiUser { id: string; name: string; } // generated from a real sample
const apiUser = (await res.json()) as ApiUser;
const user: User = { ...apiUser, id: Number(apiUser.id) }; // explicit conversion
TS2322 in React
React surfaces TS2322 in a few recurring spots:
// 1) useState infers from the initial value
const [user, setUser] = useState(null); // inferred type: null
setUser({ id: 1, name: "Ada" }); // ❌ TS2322
const [user2, setUser2] = useState<User | null>(null); // ✅ type it
// 2) a typed prop given the wrong type
function Badge({ count }: { count: number }) { return <span>{count}</span>; }
<Badge count="3" /> // ❌ Type 'string' is not assignable to type 'number'
<Badge count={3} /> // ✅
// 3) controlled input value can't be null
<input value={name} /> // if name is string | null -> TS2322; use {name ?? ""}
Common variants of this message
| Variant | Typical cause | Fix |
|---|---|---|
Type 'string' is not assignable to type 'number' | wrong primitive type | convert the value or fix the type |
Type 'string | undefined' is not assignable to type 'string' | strictNullChecks | narrow, or ?? '' |
Type 'null' is not assignable to type 'string' | null where non-null expected | allow | null or guard |
Type 'X' is not assignable to type 'never' | [] inferred as never[] | annotate the variable |
Object literal may only specify known properties | excess property | remove/rename the extra key |
'string' is not assignable to type '"asc" | "desc"' | literal union | type the source or as const |
Debugging checklist
- ✓ Read to the innermost error line — it names the real field and types
- ✓ Decide which is correct (value or declared type) and fix that one
- ✓
... | undefined/| null? Narrow with a guard or??default - ✓ Both types valid? Widen the target to a union
- ✓ React
useState? Give it a type argument:useState<T | null>(null) - ✓ From API JSON? Generate the interface with JSON to TypeScript and convert deliberately
- ✓
asonly when you truly know more than the compiler
Frequently Asked Questions
What does "is not assignable to type 'never'" mean?
'never' usually means TypeScript inferred an empty type. The classic case is const arr = [] which is inferred as never[], so pushing or assigning a value fails. Annotate the variable — const arr: number[] = [] — so it isn't never. It also appears in the impossible branch of an exhaustive switch.
Why does the error say "Types of property 'x' are incompatible"?
That nested line is the real cause. The two outer types differ only because one inner property doesn't match. Read down to the innermost Type 'A' is not assignable to type 'B' — it names the exact field and the exact types to fix, instead of the broad outer types.
Does TS2322 fail the build or only show in the editor?
Both. tsc exits with a non-zero code on TS2322, so it fails CI and production builds, and the editor underlines it. They share the same type-checker, so fixing the type clears both. A red squiggle with a passing build usually means the editor and tsconfig disagree on settings.
How do I fix TS2322 on a React useState?
Give useState an explicit type argument. useState(null) infers the state type as null, so a later setUser({...}) is not assignable. Write useState<User | null>(null) so the state accepts the values you will actually set.
Should I suppress TS2322 with @ts-expect-error?
Prefer fixing the type. If you must suppress temporarily, @ts-expect-error is better than @ts-ignore because it itself errors if the line later stops having an error, so it self-cleans. Keep either rare, on the single offending line, and commented with why.
Why is 'unknown' not assignable to a specific type?
unknown is the type-safe top type: you must narrow it before assigning it to anything specific. Validate or guard the value (typeof, an in check, or a schema parse) and TypeScript narrows it, then the assignment is allowed. any would let it through silently — that is exactly the safety unknown adds.
Generate accurate types from your JSON
Paste a real API response and get a TypeScript interface whose field types match the data — nothing is uploaded to a server.