Fix: TS2322: Type 'string' is not assignable to type
Updated 2026-03-06
TL;DR: start with the first fix section and run the verification command before changing anything else.
The Error
error TS2322: Type 'string' is not assignable to type '"success" | "error" | "pending"'.
src/api.ts:12:3 - error TS2322: Type 'string' is not assignable to type 'Status'.
12 status: getUserStatus(),
~~~~~~
Or the more general form:
Type 'string' is not assignable to type 'X'.
Where X is a string literal type, a union of literals, an enum, or a branded type. You’ll see this in your IDE as a red squiggly and in tsc output during builds.
The Fix
- Use
as constto narrow string literals. This is the most common fix. When TypeScript infers astringbut you need a specific literal type:
// Problem: TypeScript infers "success" as string
const config = {
status: "success", // type: string
method: "GET" // type: string
};
// Fix: as const narrows to literal types
const config = {
status: "success", // type: "success"
method: "GET" // type: "GET"
} as const;
You can also apply as const to individual values:
type Status = "success" | "error" | "pending";
// Problem
const status = "success"; // inferred as string
const result: Status = status; // TS2322
// Fix
const status = "success" as const; // inferred as "success"
const result: Status = status; // Works
- Add explicit type annotations when the value comes from a function or variable:
type Method = "GET" | "POST" | "PUT" | "DELETE";
// Problem: function return type is string
function getMethod(): string {
return "GET";
}
const method: Method = getMethod(); // TS2322
// Fix: annotate the return type
function getMethod(): Method {
return "GET";
}
const method: Method = getMethod(); // Works
- Use
satisfiesto validate while preserving narrow types (TypeScript 4.9+):
type Config = {
env: "development" | "staging" | "production";
port: number;
};
// Problem: as const makes everything readonly, which might conflict elsewhere
// Fix: satisfies validates the shape while keeping literal types
const config = {
env: "production",
port: 3000
} satisfies Config;
// config.env is type "production", not string
// AND TypeScript verified it matches Config
- Narrow with type guards when the value is genuinely dynamic:
type Status = "success" | "error" | "pending";
function processStatus(input: string): Status {
// Type guard: validate at runtime
const validStatuses: Status[] = ["success", "error", "pending"];
if (validStatuses.includes(input as Status)) {
return input as Status;
}
throw new Error(`Invalid status: ${input}`);
}
// Safer alternative with a Set for O(1) lookups
const STATUS_SET = new Set<Status>(["success", "error", "pending"]);
function isStatus(value: string): value is Status {
return STATUS_SET.has(value as Status);
}
// Usage
const raw: string = getStatusFromAPI();
if (isStatus(raw)) {
// raw is narrowed to Status here
handleStatus(raw); // No TS2322
}
- Fix enum assignment errors. Enums have their own type, and plain strings aren’t assignable:
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT"
}
// Problem
const dir: Direction = "UP"; // TS2322
// Fix: use the enum member
const dir: Direction = Direction.Up;
// Or if the string comes from external data, cast through the enum
const raw = "UP";
const dir: Direction = raw as Direction; // Use with caution
Why This Happens
TypeScript’s type system distinguishes between the broad string type and narrow string literal types like "success". A variable of type string can hold any string, but a variable of type "success" can only hold that exact value. When you try to assign a string to a literal type, TypeScript rejects it because the broad type doesn’t guarantee the narrow constraint.
This distinction matters because TypeScript uses structural typing with literal narrowing. When you write const x = "hello", TypeScript infers x as type "hello" (a literal). But let x = "hello" infers x as type string because let variables can be reassigned. Object properties follow the let rule — they’re mutable by default, so TypeScript widens them to string.
The as const assertion tells TypeScript “this value won’t change, so keep the narrow type.” It’s the most ergonomic fix for object literals and array values. The satisfies operator (added in TypeScript 4.9) goes further — it validates the type while preserving inference, giving you the best of both worlds.
Most TS2322 errors fall into one of two categories: data you control (use as const or explicit annotations) and data you don’t control (use type guards with runtime validation). The first category is a compile-time fix. The second requires actual runtime checks because no amount of type casting can make an arbitrary string safe.
Edge Cases
React component props. This error is extremely common in React when passing string values to components with literal prop types:
type ButtonVariant = "primary" | "secondary" | "ghost";
function Button({ variant }: { variant: ButtonVariant }) {
return <button className={variant}>Click</button>;
}
// Problem: variable inferred as string
const myVariant = getVariantFromTheme(); // string
<Button variant={myVariant} /> // TS2322
// Fix: type the variable
const myVariant: ButtonVariant = getVariantFromTheme() as ButtonVariant;
// Better fix: validate at runtime
function toButtonVariant(s: string): ButtonVariant {
if (s === "primary" || s === "secondary" || s === "ghost") return s;
return "primary"; // sensible default
}
Object.keys() returns string[]. This is a TypeScript design decision that surprises everyone:
type Theme = {
primary: string;
secondary: string;
accent: string;
};
const theme: Theme = { primary: "#000", secondary: "#333", accent: "#f00" };
// Problem: Object.keys returns string[], not (keyof Theme)[]
Object.keys(theme).forEach(key => {
const value = theme[key]; // TS2322/TS7053
});
// Fix: cast the keys
(Object.keys(theme) as (keyof Theme)[]).forEach(key => {
const value = theme[key]; // Works
});
// Or use Object.entries for cleaner iteration
Object.entries(theme).forEach(([key, value]) => {
console.log(key, value); // key is string, value is string
});
API response typing. Data from fetch or axios starts as any or unknown. Don’t cast blindly:
type ApiResponse = {
status: "ok" | "error";
data: unknown;
};
// Dangerous: trusting external data
const response = await fetch("/api/data");
const json = await response.json() as ApiResponse; // No runtime validation
// Safer: validate with Zod
import { z } from "zod";
const ApiResponseSchema = z.object({
status: z.enum(["ok", "error"]),
data: z.unknown()
});
const json = ApiResponseSchema.parse(await response.json());
// json.status is correctly typed as "ok" | "error" WITH runtime validation
Template literal types. TypeScript 4.1+ supports template literal types, which can cause unexpected TS2322 errors:
type EventName = `on${Capitalize<string>}`;
// Matches: "onClick", "onHover", "onSubmit", etc.
const event: EventName = "click"; // TS2322 — missing "on" prefix
const event: EventName = "onClick"; // Works
Discriminated unions and switch exhaustiveness. If your switch statement doesn’t cover all union members, TypeScript infers the remaining type and throws TS2322 on the default branch:
type Action = "create" | "update" | "delete";
function handle(action: Action): string {
switch (action) {
case "create": return "created";
case "update": return "updated";
// Missing "delete" case
}
// TypeScript knows action is "delete" here
// If you try to assign it to string, that works
// But the function's return type fails because not all paths return
}
Add a never check for exhaustiveness:
function assertNever(x: never): never {
throw new Error(`Unexpected value: ${x}`);
}
function handle(action: Action): string {
switch (action) {
case "create": return "created";
case "update": return "updated";
case "delete": return "deleted";
default: return assertNever(action);
}
}
See Also
Was this article helpful?
Thanks for your feedback!