Fix: TS2322 Type 'X' is not assignable to type 'Y'
Part of: React & Frontend Errors
Quick Answer
How to fix TypeScript error TS2322 'Type is not assignable to type'. Covers literal types vs general types, string vs String, union types, interface compatibility, generic constraints, readonly arrays, excess property checking, discriminated unions, type assertions, type widening and narrowing, React event handlers, Promise return types, and enum mismatches.
The Error
You write TypeScript code and the compiler hits you with:
error TS2322: Type 'string' is not assignable to type '"hello" | "world"'.Or one of its many variants:
error TS2322: Type 'string' is not assignable to type 'number'.error TS2322: Type '{ name: string; age: number; email: string; }' is not assignable to type 'User'.
Object literal may only specify known properties, and 'email' does not exist in type 'User'.error TS2322: Type 'string[]' is not assignable to type 'readonly string[]'.error TS2322: Type 'Promise<void>' is not assignable to type 'void'.TS2322 is the single most common TypeScript error. It means the value you’re providing doesn’t match the type the compiler expects. The fix depends on why the types don’t match.
Why This Happens
TypeScript’s type system is structural. When you assign a value to a variable, pass an argument to a function, or return a value, TypeScript checks whether the shape of what you’re giving matches the shape of what’s expected.
TS2322 fires when those shapes don’t match. The mismatch falls into a few categories:
- Literal type vs. general type. You’re passing
stringwhere"success" | "error"is expected. - Wrong primitive type. You’re passing
stringwherenumberis expected (common with form inputs, query params, or JSON parsing). - Missing or extra properties. Your object has properties the target type doesn’t expect, or is missing required ones.
- Readonly vs. mutable. You’re passing a mutable array where a
readonlyarray is expected, or vice versa. - Generic constraint mismatch. Your generic type argument doesn’t satisfy the constraint.
- Union type incompatibility. You’re assigning a wider union to a narrower one.
null/undefinedsneaking in. The source type includesnullorundefinedbut the target doesn’t. (For deep coverage of this case, see Fix: TS2532 Object is possibly ‘undefined’.)
The error message itself tells you exactly what’s wrong. Read it carefully — Type 'A' is not assignable to type 'B' means you have an A but need a B.
Fix
1. Literal Types vs. General Types (Type Widening)
This is the most common variant. You declare a variable with let or pass a plain string, and TypeScript widens it to string instead of keeping the literal type:
type Status = 'active' | 'inactive' | 'pending';
let status = 'active';
// status is `string`, not `'active'`
const user: { status: Status } = { status };
// ~~~~~~
// Type 'string' is not assignable to type '"active" | "inactive" | "pending"'.TypeScript infers let variables as their widened type (string, number, etc.) because you might reassign them. A const variable keeps the literal type because it can never change.
Fix — use const:
const status = 'active'; // type is 'active', not stringFix — use as const:
When you can’t use const (e.g., the value comes from an expression or you need it in an object):
let status = 'active' as const; // type is 'active'
const config = {
status: 'active' as const, // type is 'active', not string
retries: 3 as const, // type is 3, not number
};Fix — use as const on the entire object:
const config = {
status: 'active',
retries: 3,
} as const;
// config.status is 'active', config.retries is 3Fix — annotate the type explicitly:
let status: Status = 'active';Fix — use satisfies (TypeScript 4.9+):
const config = {
status: 'active',
retries: 3,
} satisfies { status: Status; retries: number };
// config.status is 'active' AND validated against Statussatisfies validates the type without widening it. It’s the best of both worlds — you get compile-time checking and keep the narrow inferred type.
2. string vs String (and Other Primitive Wrappers)
TypeScript distinguishes between lowercase primitives (string, number, boolean) and their uppercase wrapper objects (String, Number, Boolean):
let name: String = 'Alice'; // works but wrong
let greeting: string = name;
// ~~~~~~~~
// Type 'String' is not assignable to type 'string'.
// 'string' is a primitive, but 'String' is a wrapper object.Fix — always use lowercase primitives:
let name: string = 'Alice';
let count: number = 42;
let active: boolean = true;Never use String, Number, Boolean, or Object as types. The uppercase versions are JavaScript wrapper objects — you almost never want them. This also applies to {} and object: use specific types or Record<string, unknown> instead.
3. Excess Property Checking (Extra Properties on Object Literals)
TypeScript applies special rules to object literals. If you pass an object literal directly, TypeScript rejects unknown properties:
interface User {
name: string;
age: number;
}
const user: User = {
name: 'Alice',
age: 30,
email: '[email protected]',
//~~~~
// Object literal may only specify known properties,
// and 'email' does not exist in type 'User'.
};This only happens with object literals assigned directly. If you assign through a variable, TypeScript allows extra properties (structural typing):
const data = { name: 'Alice', age: 30, email: '[email protected]' };
const user: User = data; // no error — extra properties are fine through a variableFix — add the property to the type:
interface User {
name: string;
age: number;
email?: string; // now it's allowed
}Fix — use a type assertion:
const user = {
name: 'Alice',
age: 30,
email: '[email protected]',
} as User;This suppresses the error but also suppresses checking on the properties that User does define. Use sparingly.
Fix — use an intermediate variable:
const data = { name: 'Alice', age: 30, email: '[email protected]' };
const user: User = data; // no excess property checkThis is technically valid TypeScript, but it often indicates your type definition is incomplete. Prefer updating the type.
4. Union Type Mismatches
You can’t assign a wider type to a narrower union:
type Color = 'red' | 'green' | 'blue';
function paint(color: Color) { ... }
const input: string = getColorFromUser();
paint(input);
// ~~~~~
// Type 'string' is not assignable to type '"red" | "green" | "blue"'.Fix — validate and narrow the input:
function isColor(value: string): value is Color {
return ['red', 'green', 'blue'].includes(value);
}
const input = getColorFromUser();
if (isColor(input)) {
paint(input); // narrowed to Color
}Fix — assert the type if you’re certain:
paint(input as Color); // you take responsibility for correctness5. Interface and Type Compatibility (Missing Properties)
When an object is missing required properties:
interface Config {
host: string;
port: number;
ssl: boolean;
}
const config: Config = {
host: 'localhost',
port: 3000,
// error: Property 'ssl' is missing in type '{ host: string; port: number; }'
};Fix — add the missing property:
const config: Config = {
host: 'localhost',
port: 3000,
ssl: false,
};Fix — make the property optional in the type:
interface Config {
host: string;
port: number;
ssl?: boolean; // now optional
}Fix — use Partial<T> if all properties should be optional:
function updateConfig(overrides: Partial<Config>) { ... }
updateConfig({ port: 8080 }); // fine — all properties are optional6. Readonly Arrays and Tuples
A mutable array is not assignable to a readonly array in the reverse direction:
function process(items: readonly string[]) { ... }
const mutable: string[] = ['a', 'b'];
process(mutable); // fine — mutable is assignable to readonly
function mutate(items: string[]) { ... }
const immutable: readonly string[] = ['a', 'b'];
mutate(immutable);
// ~~~~~~~~~
// Type 'readonly string[]' is not assignable to type 'string[]'.
// The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.TypeScript prevents this because mutate could push to the array, violating the readonly contract.
Fix — accept readonly in the function signature:
If the function doesn’t modify the array, mark the parameter as readonly:
function mutate(items: readonly string[]) {
// now you can't push/pop/splice, but that's correct if you don't need to
items.forEach(item => console.log(item));
}Fix — copy the array:
mutate([...immutable]); // creates a mutable copy7. Generic Constraint Mismatches
When a generic type doesn’t satisfy its constraint:
function merge<T extends object>(target: T, source: Partial<T>): T {
return { ...target, ...source };
}
merge('hello', {});
// ~~~~~~~
// Type 'string' is not assignable to type 'object'.Fix — pass a value that satisfies the constraint:
merge({ name: 'Alice' }, { name: 'Bob' }); // T extends object: worksFix — relax the constraint:
function merge<T>(target: T, source: Partial<T>): T { ... }A more subtle case — when your generic function returns a value that doesn’t satisfy the constraint:
function createDefault<T extends { id: string }>(): T {
return { id: 'default' };
// ~~~~~~~~~~~~~~~~
// Type '{ id: string; }' is not assignable to type 'T'.
// '{ id: string; }' is assignable to the constraint, but 'T' could have more properties.
}This is correct behavior. T could be { id: string; name: string }, which { id: 'default' } doesn’t satisfy. Fix by returning the constraint type instead of the generic:
function createDefault(): { id: string } {
return { id: 'default' };
}8. Discriminated Union Mismatches
When working with discriminated unions, you must include the discriminant property with the correct literal value:
type Result =
| { status: 'success'; data: string }
| { status: 'error'; message: string };
const result: Result = {
status: 'success',
message: 'something went wrong',
//~~~~~~~
// Type '{ status: "success"; message: string; }' is not assignable to type 'Result'.
};TypeScript narrows the union based on status: 'success' and finds message doesn’t belong on the success variant.
Fix — match the correct variant shape:
const result: Result = {
status: 'success',
data: 'some data', // correct property for 'success'
};Common Mistake: Using
as constonly on individual properties when you need it on the entire object. If you have an object with multiple literal values that all need to stay narrow, applyas constto the whole object rather than annotating each property separately.
9. Event Handler Types in React
React event handlers have specific types. Plain DOM event types don’t match:
function handleChange(e: Event) { ... }
// ~
// not the right type for React
<input onChange={handleChange} />
// ~~~~~~~~
// Type '(e: Event) => void' is not assignable to type 'ChangeEventHandler<HTMLInputElement>'.Fix — use React’s event types:
import { ChangeEvent } from 'react';
function handleChange(e: ChangeEvent<HTMLInputElement>) {
console.log(e.target.value);
}Common React event type mappings:
| Event | React Type |
|---|---|
onChange | ChangeEvent<HTMLInputElement> |
onClick | MouseEvent<HTMLButtonElement> |
onSubmit | FormEvent<HTMLFormElement> |
onKeyDown | KeyboardEvent<HTMLInputElement> |
onFocus | FocusEvent<HTMLInputElement> |
onDrag | DragEvent<HTMLDivElement> |
Fix — let TypeScript infer the type inline:
<input onChange={(e) => console.log(e.target.value)} />
// TypeScript infers e as ChangeEvent<HTMLInputElement> automatically10. Promise Return Type Mismatches
Async functions return Promise<T>, not T. This trips you up when defining callback types or interface methods:
interface DataLoader {
load: () => string[];
}
const loader: DataLoader = {
load: async () => {
// ~~~~
// Type '() => Promise<string[]>' is not assignable to type '() => string[]'.
const data = await fetchData();
return data;
},
};Fix — update the type to expect a Promise:
interface DataLoader {
load: () => Promise<string[]>;
}Fix — don’t use async if you don’t need to:
const loader: DataLoader = {
load: () => getCachedData(), // synchronous, returns string[]
};The reverse also happens — assigning a sync function where an async one is expected works fine (a string[] is assignable to Promise<string[]> in some contexts), but returning Promise<string[]> where string[] is expected never works. You can’t unwrap a Promise without await.
11. Enum Mismatches
TypeScript enums are nominally typed. Even if two enums have the same values, they’re not interchangeable:
enum Color {
Red = 'RED',
Blue = 'BLUE',
}
enum Theme {
Red = 'RED',
Blue = 'BLUE',
}
let color: Color = Theme.Red;
// ~~~~~
// Type 'Theme.Red' is not assignable to type 'Color'.Fix — use the correct enum:
let color: Color = Color.Red;Fix — use string literal unions instead of enums:
String literal unions don’t have this problem and are generally simpler:
type Color = 'RED' | 'BLUE';
type Theme = 'RED' | 'BLUE';
let color: Color = 'RED'; // works
let theme: Theme = color; // works — both are just stringsNumeric enums have an additional pitfall — they’re assignable to number and vice versa, which can lead to bugs:
enum Direction {
Up, // 0
Down, // 1
}
let dir: Direction = 99; // no error! TypeScript allows any numberThis is a known design limitation. String enums are safer. Or use as const objects:
const Direction = {
Up: 'UP',
Down: 'DOWN',
} as const;
type Direction = typeof Direction[keyof typeof Direction]; // 'UP' | 'DOWN'12. Type Assertions (as)
When you know more than TypeScript about a value’s type, use a type assertion:
const input = document.getElementById('name') as HTMLInputElement;
input.value = 'Alice'; // no error — you told TypeScript it's an HTMLInputElementType assertions don’t perform any runtime conversion. They only tell the compiler to treat a value as a different type. If you’re wrong, you’ll crash at runtime.
Double assertion for incompatible types:
Sometimes TypeScript won’t let you assert directly between unrelated types:
const value: string = 'hello';
const num = value as number;
// ~~~~~
// Conversion of type 'string' to type 'number' may be a mistake.You can force it with a double assertion through unknown:
const num = value as unknown as number;This is almost always a code smell. If you need this, you probably have a design problem. But it’s useful in tests or when working with poorly typed third-party code.
13. Index Signature Mismatches
Objects with index signatures have specific assignability rules:
interface StringMap {
[key: string]: string;
}
const map: StringMap = {
name: 'Alice',
age: 30,
// ~~
// Type 'number' is not assignable to type 'string'.
};Every property must conform to the index signature.
Fix — convert values to the correct type:
const map: StringMap = {
name: 'Alice',
age: String(30), // '30'
};Fix — use a more flexible type:
interface FlexibleMap {
[key: string]: string | number;
}Fix — use Record for clarity:
const map: Record<string, string> = {
name: 'Alice',
age: '30',
};14. Function Parameter Bivariance
Function types are checked in a way that can surprise you. A function with fewer parameters is assignable to one with more (parameter dropping is safe), but parameter types follow specific rules:
type Handler = (event: MouseEvent) => void;
const handler: Handler = (event: Event) => { ... };
// ~~~~~~~
// Type '(event: Event) => void' is not assignable to type '(event: MouseEvent) => void'.
// Type 'Event' is not assignable to type 'MouseEvent'.With strictFunctionTypes enabled (included in strict: true), function parameter types are checked contravariantly. A handler expecting Event (supertype) can’t substitute for one expecting MouseEvent (subtype), because the handler might not use MouseEvent-specific properties.
Fix — use the correct parameter type:
const handler: Handler = (event: MouseEvent) => {
console.log(event.clientX); // MouseEvent-specific property
};How Other Type Systems Handle Assignment
TypeScript’s structural typing shapes every TS2322 you will ever see. Other typed-JavaScript systems make different trade-offs, and knowing them sharpens your intuition about why TS rejects something a different checker would accept.
TypeScript (structural / duck typed): Two types are compatible if they have the same shape. A literal { name: string; age: number } is assignable to User if User has those fields, even if neither side knows about the other. Excess property checking on object literals is the one nominal-ish exception, and it only fires on direct literal assignment — going through a variable bypasses it.
Flow (Facebook): Also structural, but with stricter variance rules. Read-only properties are covariant, writable properties are invariant. Flow’s object types are exact by default with the {| ... |} syntax — equivalent to TypeScript’s excess property check, but enforced everywhere, not only on literals. The same Type 'X' is not assignable to type 'Y' error in Flow is phrased as Cannot assign X to Y.
Hegel: A community type-checker that emphasizes nominal typing for opaque types and stronger inference for generics. Hegel rejects more code than TS because it does not have any — every value has a knowable type at every point. Where TS prints TS2322, Hegel typically prints a more specific narrowing-related error before assignment is even attempted.
ReScript / OCaml-derived inference: Soundly typed. The compiler infers types globally instead of locally, so you rarely annotate. Assignment errors are reported by Hindley-Milner unification: “this has type string but somewhere wanted int” — and the “somewhere” is computed by the inference engine, not by reading your annotations. A literal-vs-general-type widening problem in TS does not exist in ReScript because there is no string “widening” step.
Plain JSDoc + tsc --checkJs: TypeScript itself, but reading types from @type comments in .js files. Same structural rules, same TS2322 messages. The catch is that @type annotations get widened to the JSDoc type’s general form unless you write @type {const} or use @satisfies (TypeScript 5.0+).
Narrowing patterns are the cross-cutting fix. Whatever the system, the way out of an assignability error is almost always to narrow the source type rather than widen the target. TypeScript-specific narrowing tools: user-defined type guards (value is X), discriminated union switches, the in operator, instanceof, satisfies, and the assert family from Node 16+. Reach for these before as. A type assertion silences the compiler without changing the runtime, which is fine when you genuinely know more than the checker, and a footgun when you do not.
If you are migrating from Flow or another system, the rule of thumb is: TS will accept more code at the boundaries (extra properties through a variable, structural duck-typing) and reject more code in generics (variance is enforced under strictFunctionTypes).
Still Not Working?
Types Look Identical But Still Incompatible
If TypeScript says Type 'X' is not assignable to type 'X' where both types have the same name, you have duplicate type definitions. This happens when:
- Two versions of
@types/packages are installed (e.g.,@types/react@17and@types/react@18in the same project). - A monorepo has the same type defined in multiple packages.
- You import a type from a re-exported path vs. the original package.
Check for duplicates:
npm ls @types/react
npm ls @types/nodeFix with overrides in package.json:
{
"overrides": {
"@types/react": "^18.2.0"
}
}Then run npm install to deduplicate.
as const Objects Not Working With Function Parameters
You create an as const object but a function still rejects it:
const options = { method: 'GET', headers: {} } as const;
fetch('/api', options);
// ~~~~~~~
// Type '{ readonly method: "GET"; readonly headers: {}; }' is not assignable...The readonly modifier on every property can conflict with function signatures expecting mutable types. Fix by spreading into a new object:
fetch('/api', { ...options });Or assert the specific property:
fetch('/api', { method: 'GET' as const, headers: {} });Conditional Types Resolving to never
If a conditional type resolves to never, any assignment to it fails:
type ExtractString<T> = T extends string ? T : never;
type Result = ExtractString<number>; // never
const value: Result = 'hello';
// ~~~~~
// Type 'string' is not assignable to type 'never'.never means “no value can exist here.” Check your conditional type logic — the condition isn’t matching the type you’re passing.
Template Literal Types
TypeScript 4.1+ template literal types can cause TS2322 when string formats don’t match:
type EventName = `on${Capitalize<string>}`;
const event: EventName = 'onclick';
// ~~~~~
// Type '"onclick"' is not assignable to type '`on${Capitalize<string>}`'.Capitalize<string> expects the first character after on to be uppercase.
Fix:
const event: EventName = 'onClick'; // capital CMapped Type Incompatibility
When you use Pick, Omit, or custom mapped types, the resulting type might not match what you expect:
interface User {
id: string;
name: string;
email: string;
}
type CreateUser = Omit<User, 'id'>;
const user: User = { name: 'Alice', email: '[email protected]' };
// ~~~~
// Property 'id' is missing in type '{ name: string; email: string; }'You annotated user as User, not CreateUser. The error message tells you exactly what’s missing.
Fix — use the correct type:
const user: CreateUser = { name: 'Alice', email: '[email protected]' };A Library Updated Its .d.ts Without a Major Version Bump
A patch release of a typed library can tighten a parameter type and surface TS2322 in your code that was clean yesterday. Common offenders: @types/react patch bumps, @types/node event API changes, and SDK shims that switched string to a literal union. Pin the type package to a known-good version with overrides in package.json until you have time to update your callers. Diff the .d.ts between the old and new version (npm view <pkg>@<old> dist.tarball vs the new) to see exactly what tightened.
strictNullChecks was Toggled on a Subset of Files
If your project uses a single tsconfig.json but a downstream package compiles with different strictness, the published .d.ts files can include undefined in places yours does not — or strip it out. The TS2322 then comes from the imported type, not your code. Run tsc --traceResolution and check whether the offending type is coming from the package’s own .d.ts or from @types/<pkg>. Fixing the wrong one wastes hours.
A Generic Inference Site is Defaulting to {}
When TypeScript cannot infer a generic parameter, it falls back to {} (or unknown under --noImplicitAny). This frequently happens with callback signatures: Array.prototype.reduce with no initial value, Promise.all over a union type, or a curried function. The TS2322 then complains that {} is not assignable to your expected type. Explicit type arguments on the call site (reduce<MyAccType>(...)) fix it cleanly — do not work around it by typing the callback parameter.
exactOptionalPropertyTypes Surfaces a New TS2322
Turning on exactOptionalPropertyTypes in tsconfig.json makes { name?: string } reject { name: undefined } — they are no longer the same type. Code that previously assigned undefined to an optional field now fails. Either drop the explicit undefined (let the property be absent) or change the field signature to name?: string | undefined.
TypeScript Version Mismatch Between Editor and Build
Your editor might use a different TypeScript version than your build tool. VS Code bundles its own TypeScript. If your project uses TypeScript 5.4 but VS Code uses 5.2, you’ll see phantom errors.
Force VS Code to use your project’s TypeScript:
- Open the command palette (
Ctrl+Shift+P/Cmd+Shift+P). - Run “TypeScript: Select TypeScript Version”.
- Choose “Use Workspace Version”.
If your project has no errors with npx tsc --noEmit but your editor still shows TS2322, this is almost certainly the cause.
Related: Fix: TS2532 Object is possibly ‘undefined’ | Fix: TS2307 Cannot find module or its corresponding type declarations | Fix: TypeError: Cannot read properties of undefined | Fix: ESLint Parsing error: Unexpected token
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: i18next Not Working — Translations Missing, Language Not Switching, or Namespace Errors
How to fix i18next issues — react-i18next setup, translation file loading, namespace configuration, language detection, interpolation, pluralization, and Next.js integration.
Fix: Mapbox GL JS Not Working — Map Not Rendering, Markers Missing, or Access Token Invalid
How to fix Mapbox GL JS issues — access token setup, React integration with react-map-gl, markers and popups, custom layers, geocoding, directions, and Next.js configuration.
Fix: React PDF Not Working — PDF Not Rendering, Worker Error, or Pages Blank
How to fix react-pdf and @react-pdf/renderer issues — PDF viewer setup, worker configuration, page rendering, text selection, annotations, and generating PDFs in React.
Fix: React Three Fiber Not Working — Canvas Blank, Models Not Loading, or Performance Dropping
How to fix React Three Fiber (R3F) issues — Canvas setup, loading 3D models with useGLTF, lighting, camera controls, animations with useFrame, post-processing, and Next.js integration.