TypeScript shows Type 'X' is not assignable to type 'Y' (TS2322), or Argument of type 'X' is not assignable to parameter of type 'Y' (TS2345), when the value on the right could hold something the type on the left does not allow. Most often that something is undefined, null, a plain string where a literal union was expected, or an un-awaited Promise. Fix it by narrowing the value, converting it, or correcting the declared type. Do not reach for as or any first.
This is the TypeScript error I hit most, and its long message makes it easy to read the wrong half. The first line names the two types. The indented lines underneath explain exactly which part does not fit. Once you know how to read that chain, most fixes take seconds.
I reproduced every case below on my machine with TypeScript 7.0.2 (the new native compiler) and Node v24.22.1, compiling each file on its own with --strict --noEmit. Two cases were also checked on TypeScript 5.9.3, and the messages were identical. Everything inside the output blocks is copied straight from those runs.
In this guide:
- What the error looks like
- Why TypeScript refuses the assignment
- TypeScript 7 made this error more common
- The 10 causes at a glance
- 1. An optional property or parameter (string | undefined)
- 2. A string from a form or URL passed where a number is expected
- 3. A plain string where a literal union is expected
- 4. A required property is missing from an object
- 5. A DOM query that can return null
- 6. Forgetting to await a Promise
- 7. An array with mixed types
- 8. A readonly array passed to a function that expects a mutable one
- 9. React useState(null) without a type
- 10. Returning a concrete value from a generic function
- The fixes in one file, compiled and run
- Why "as" is the wrong first fix (and satisfies is better)
- How to fix it fast: a checklist
- Frequently asked questions
What the error looks like
Here is the smallest example: an optional property assigned to a variable typed as string.
interface User { name: string; nickname?: string; } function greet(user: User): string { const label: string = user.nickname; return `Hi ${label}`; } c01_optional.ts(7,9): error TS2322: Type 'string | undefined' is not assignable to type 'string'. Type 'undefined' is not assignable to type 'string'. Read the output in three parts. c01_optional.ts(7,9) is the file, line and column. TS2322 is the error code you can search for. The indented line is the important one: TypeScript tells you that the undefined member of the union is the piece that does not fit into string.
On longer errors, always read to the last indented line. That is the actual conflict.
Why TypeScript refuses the assignment
TypeScript checks assignability structurally. A value of type A can go into a slot of type B only if every possible A is also a valid B. string | undefined fails that test because one possible value, undefined, is not a string. The type compatibility chapter of the handbook describes these rules in detail.
The same check runs in three places, which is why you see slightly different codes:
- TS2322 for assignments, return values and variable declarations.
- TS2345 for function arguments.
- TS2741, TS2353 and TS4104 for more specific object and readonly mismatches that come from the same rule.
TypeScript 7 made this error more common
One important 2026 change: the TypeScript 7.0 release turns strict on by default. In my test, the same file with no flags at all still failed on TS 7.0.2, passed on TS 7.0.2 with --strict false, and passed on TS 5.9.3 with default options. So if you just upgraded and dozens of these errors appeared, that is the reason.
c01_optional.ts(7,9): error TS2322: Type 'string | undefined' is not assignable to type 'string'. Type 'undefined' is not assignable to type 'string'. With --strict false on TS 7.0.2 and with default options on TS 5.9.3, the same file printed nothing and exited with code 0. The errors were always real bugs waiting to happen; strict mode just stopped hiding them.
The 10 causes at a glance
Find the row that matches your message.
| # | What you see | Code | Usual fix |
|---|---|---|---|
| 1 | `'string | undefined' is not assignable to 'string'` | TS2322 |
| 2 | Argument of type 'string' ... parameter of type 'number' | TS2345 | Convert with Number() and validate |
| 3 | Argument of type 'string' ... parameter of type 'Direction' | TS2345 | const, a type annotation, as const or satisfies |
| 4 | Property 'price' is missing ... but required | TS2741 | Add the property or make it optional |
| 5 | `'HTMLInputElement | null'` is not assignable | TS2322 |
| 6 | 'Promise<number>' is not assignable to 'number' | TS2322 | await the call |
| 7 | `'(string | number)[]' is not assignable to 'string[]'` | TS2322 |
| 8 | readonly ... cannot be assigned to the mutable type | TS4104 | Accept readonly string[] |
| 9 | ... not assignable to parameter of type 'SetStateAction<null>' | TS2345 | `useState<User |
| 10 | {% raw %}'T' could be instantiated with an arbitrary type | TS2322 | Let the caller pass the default |
1. An optional property or parameter (string | undefined)
This is the example above. nickname?: string really means string | undefined, so it cannot go straight into a string. The same happens with optional function parameters, Array.prototype.find(), Map.get() and environment variables like process.env.API_URL.
Give it a fallback with the nullish coalescing operator ??, or check it first:
const label: string = user.nickname ?? user.name; If you are unsure whether the value should ever be missing, the real question is about your data, not your types. My guide to null vs undefined in JavaScript covers when each one shows up.
2. A string from a form or URL passed where a number is expected
Values from inputs, query strings, localStorage and CSV files are always strings, even when they look like numbers.
function addVat(price: number): number { return price * 1.2; } const input = "49.99"; // e.g. from a form field or URL param addVat(input); c02_input_string.ts(6,8): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. Convert at the edge of your program and check the result, so the rest of your code only sees real numbers:
const price = Number(input); if (!Number.isNaN(price)) addVat(price); Avoid input as unknown as number. It compiles, but at runtime "49.99" * 1.2 only works by accident, and "49.99" + 1 gives "49.991".
3. A plain string where a literal union is expected
This one confuses almost everyone the first time. The value is literally "left", yet TypeScript still complains:
type Direction = "left" | "right"; function move(dir: Direction) { console.log("moving", dir); } let dir = "left"; move(dir); const config = { dir: "right" }; move(config.dir); c03_literal_widening.ts(8,6): error TS2345: Argument of type 'string' is not assignable to parameter of type 'Direction'. c03_literal_widening.ts(11,6): error TS2345: Argument of type 'string' is not assignable to parameter of type 'Direction'. The reason is widening. A let variable can be reassigned, so TypeScript infers string, not "left". Object properties are mutable too, so config.dir also widens to string. The handbook section on literal types and literal inference explains the rule.
Any of these fixes passed in my test file:
const dir = "left"; move(dir); let dir2: Direction = "left"; move(dir2); const config = { dir: "right" } as const; move(config.dir); const config2 = { dir: "right" } satisfies { dir: Direction }; move(config2.dir); I prefer satisfies for config objects. It checks the object against the type and still keeps the narrow literal types.
4. A required property is missing from an object
When an object literal does not match its interface, TypeScript names the missing property directly:
interface Product { id: number; title: string; price: number; } const p: Product = { id: 1, title: "Mug" }; c04_missing_prop.ts(7,7): error TS2741: Property 'price' is missing in type '{ id: number; title: string; }' but required in type 'Product'. Either add the property, or, if it truly is optional in your data, change the interface to price?: number. Making it optional just moves the problem to every place that reads price, so only do it when the data really can be missing. If you are deciding how to model these shapes in the first place, see TypeScript interface vs type.
5. A DOM query that can return null
document.querySelector() and getElementById() return null when nothing matches, and the DOM types say so:
const input: HTMLInputElement = document.querySelector("#email"); c05_null_dom.ts(1,7): error TS2322: Type 'HTMLInputElement | null' is not assignable to type 'HTMLInputElement'. Type 'null' is not assignable to type 'HTMLInputElement'. Pass the element type as a generic and check for null before you use it. MDN's querySelector reference confirms the null return value.
const el = document.querySelector<HTMLInputElement>("#email"); if (el) el.value = "test@example.com"; The non-null assertion querySelector("#email")! also compiles. Use it only when the element is guaranteed by your own HTML, because if the selector is wrong you get a Cannot read properties of null TypeError at runtime instead.
6. Forgetting to await a Promise
An async function always returns a Promise, never the bare value:
async function getCount(): Promise<number> { return 42; } function show() { const count: number = getCount(); console.log(count); } c06_promise.ts(6,9): error TS2322: Type 'Promise<number>' is not assignable to type 'number'. Make the calling function async and await the call:
async function show() { const count: number = await getCount(); return count; } Without the type annotation, count would quietly be a Promise and you would print Promise { 42 } instead of the number. My async/await tutorial walks through the patterns.
7. An array with mixed types
TypeScript infers the element type from everything in the array literal:
const mixed = ["a", 1, "b"]; const names: string[] = mixed; c07_union_array.ts(2,7): error TS2322: Type '(string | number)[]' is not assignable to type 'string[]'. Type 'string | number' is not assignable to type 'string'. Type 'number' is not assignable to type 'string'. This output shows a three-level chain. The bottom line, Type 'number' is not assignable to type 'string', is the real problem: there is a number in the array. Either fix the data, declare the array as (string | number)[] and handle both cases, or filter it with a type guard such as mixed.filter((x): x is string => typeof x === "string").
8. A readonly array passed to a function that expects a mutable one
as const makes an array readonly, and TypeScript will not let a function that might call push() receive it:
const COLORS = ["red", "green"] as const; function paint(colors: string[]) { colors.push("blue"); } paint(COLORS); c08_readonly.ts(7,7): error TS4104: The type 'readonly ["red", "green"]' is 'readonly' and cannot be assigned to the mutable type 'string[]'. Here the message has a different code, TS4104, but it is the same assignability rule. If the function does not need to change the array, change its parameter to readonly string[], which accepts both mutable and readonly arrays. If it does need to change it, pass a copy: paint([...COLORS]). The handbook's object types page covers readonly arrays and tuples.
9. React useState(null) without a type
This is the React version of the error, and it is everywhere in Next.js projects. useState(null) infers the state type as null, forever:
import { useState } from "react"; type User = { id: number; name: string }; export function useUser(data: User) { const [user, setUser] = useState(null); setUser(data); setUser({ id: 1, name: "Mahir" }); return user; } c09_usestate.ts(7,11): error TS2345: Argument of type 'User' is not assignable to parameter of type 'SetStateAction<null>'. Type 'User' provides no match for the signature '(prevState: null): null'. c09_usestate.ts(8,13): error TS2353: Object literal may only specify known properties, and 'id' does not exist in type '(prevState: null) => null'. Notice that passing a variable gives TS2345, while passing an object literal gives TS2353. Both have the same cause. Tell useState what the state can hold, as the React useState reference shows for TypeScript:
import { useState } from "react"; type User = { id: number; name: string }; export function useUser() { const [user, setUser] = useState<User | null>(null); setUser({ id: 1, name: "Mahir" }); return user; } That file compiled with exit code 0 against @types/react 19. The same fix applies to useState([]), which infers never[]; write useState<Item[]>([]).
10. Returning a concrete value from a generic function
This is the most confusing message of the ten:
function withDefault<T>(value: T | undefined): T { if (value === undefined) { return "default"; } return value; } c10_generic.ts(3,5): error TS2322: Type 'string' is not assignable to type 'T'. 'T' could be instantiated with an arbitrary type which could be unrelated to 'string'. The function promises to return a T, and the caller chooses T. If someone calls withDefault<number>(undefined), returning "default" would break that promise. The generics handbook page covers this. The fix is to let the caller supply the fallback, so it is always a T:
function withDefault<T>(value: T | undefined, fallback: T): T { return value === undefined ? fallback : value; } The fixes in one file, compiled and run
I collected the fixes for causes 1, 2, 3, 5, 6, 8 and 10 into a single file, f01_fixes.ts, using exactly the snippets shown above, to prove they compile together under strict mode and behave correctly at runtime. The compiler result:
[exit 0] Exit code 0 with no messages means no type errors. Compiling to JavaScript and running it in Node printed:
Hi Mahir 12 right 2 5 count 42 The DOM block is wrapped in a typeof document check only because this test ran in Node, which has no document.
Why "as" is the wrong first fix (and satisfies is better)
A type assertion tells TypeScript to trust you. It removes the error but not the bug:
interface Product { id: number; title: string; price: number } // "as" silences the compiler, and the bug ships const risky = { id: 1, title: "Mug" } as Product; console.log(risky.price.toFixed(2)); [exit 0] It compiled cleanly. Then Node crashed when the code ran, with the classic Cannot read properties of undefined error:
f03_as_vs_satisfies.js:4 console.log(risky.price.toFixed(2)); ^ TypeError: Cannot read properties of undefined (reading 'toFixed') at Object.<anonymous> (f03_as_vs_satisfies.js:4:25) The satisfies operator, added in TypeScript 4.9, checks the object without changing its type, so the same mistake is caught before it ships:
interface Product { id: number; title: string; price: number } const checked = { id: 1, title: "Mug" } satisfies Product; f04_satisfies.ts(3,41): error TS2741: Property 'price' is missing in type '{ id: number; title: string; }' but required in type 'Product'. My rule of thumb: as is fine for genuinely unknowable things, like parsed JSON you validate elsewhere. For anything you wrote yourself, prefer an annotation or satisfies. And any switches off checking for everything the value touches, so treat it as a last resort.
How to fix it fast: a checklist
- Read the last indented line of the message. That is the exact part that does not fit.
- If it mentions
undefinedornull, add a check, a default with??, or fix the data source. - If it says
stringwhere a union like'left' | 'right'is expected, useconst, an annotation,as constorsatisfies. - If it mentions
Promise, addawait. - If it mentions
neverornullinside React state, giveuseStatea type argument. - If it mentions
readonly, widen the parameter toreadonly T[]. - If it mentions a generic
T, stop returning a fixed value from a generic function. - Only then consider
as, and never use it to paper overundefined.
If you just upgraded to TypeScript 7 and the error count exploded, you can set "strict": false in tsconfig.json temporarily and then turn the individual strict checks back on one by one. For quick formatting and conversion jobs while you debug, my free browser tools work right in your browser.
Frequently asked questions
What does Type 'X' is not assignable to type 'Y' mean?
It means the value on the right might hold something that the type on the left does not allow. TypeScript checks every possible value of X, so even one member, like undefined, is enough to fail.
What is the difference between TS2322 and TS2345?
They are the same assignability check in different places. TS2322 is reported for assignments, declarations and return values. TS2345 is reported when you pass an argument to a function parameter.
How do I fix string | undefined is not assignable to type string?
Narrow the value first. Use a default with ??, an if check that returns early, or fix the source so the value is never missing. A non-null assertion with ! compiles too, but it only moves the risk to runtime.
Why did these errors appear after upgrading to TypeScript 7?
TypeScript 7.0 enables strict mode by default. In my test the same file failed on 7.0.2 with no flags and passed on 5.9.3 with default options. Set strict to false temporarily if you need time to migrate.
Is it OK to fix the error with as or any?
Only when you truly know more than the compiler, such as validated JSON. In my test an as Product assertion compiled cleanly and then crashed at runtime with Cannot read properties of undefined. satisfies caught the same mistake at compile time.
Why does useState(null) cause not assignable errors in React?
TypeScript infers the state type from the initial value, so useState(null) can only ever hold null. Write useState(null) so the setter accepts a User.
Last checked: 9 November 2026. Figures, settings and guidance can change, so confirm anything important with the sources linked above.
Originally published on mfaysal.com.
I'm learning cybersecurity on TryHackMe — follow my progress at tryhackme.com/p/mfaysal.dev.