Fix: TS2322 Type 'X' is not assignable to type 'Y'
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: 'alice@example.com',
//~~~~
// 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: 'alice@example.com' };
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: 'alice@example.com',
} 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: 'alice@example.com' };
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'
};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
};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: 'alice@test.com' };
// ~~~~
// 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: 'alice@test.com' };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 | Fix: React Hooks called conditionally
Related Articles
Fix: TS2532 Object is possibly 'undefined' / Object is possibly 'null'
How to fix TypeScript errors TS2532 'Object is possibly undefined', TS18048 'Object is possibly undefined', and 'Object is possibly null'. Covers optional chaining, nullish coalescing, type narrowing, non-null assertion, type guards, strictNullChecks, Array.find, Map.get, React useRef, and more.
Fix: Module not found: Can't resolve / Cannot find module or its corresponding type declarations
How to fix 'Module not found: Can't resolve' in webpack, Vite, and React, and 'Cannot find module or its corresponding type declarations' in TypeScript. Covers missing packages, wrong import paths, case sensitivity, path aliases, node_modules corruption, monorepo hoisting, barrel files, and asset imports.
Fix: [vite] Internal server error: Failed to resolve import
How to fix Vite's 'Failed to resolve import' error, including 'Does the file exist?', 'Optimized dependency needs to be force included', 'Pre-transform error', and '504 (Outdated Optimize Dep)'. Covers missing packages, path aliases, optimizeDeps, cache clearing, and CJS/monorepo edge cases.
Fix: TypeError: Cannot read properties of undefined (reading 'xxx')
How to fix 'TypeError: Cannot read properties of undefined', 'Cannot read property of undefined', and 'Cannot read properties of null' in JavaScript, TypeScript, and React. Covers optional chaining, async data fetching, destructuring, and nested object access.