The TypeScript error Type 'string' is not assignable to type ... looks simple, but it appears in several different situations: widened literals, narrow unions, enums, object properties, function arguments, and values coming from APIs or form inputs. This guide gives you a practical way to diagnose the error quickly, fix it without weakening your types, and recognize when the right answer is narrowing, better modeling, or a small refactor instead of an assertion.
Overview
This article helps you fix one of the most common TypeScript assignability errors without turning off type safety. The short version is this: TypeScript is telling you that a general string could contain values outside the narrower set that a target type allows.
For example, this is valid TypeScript reasoning:
type Status = 'draft' | 'read' | 'unread';
let value: string = 'draft';
let status: Status = value; // ErrorEven though value currently holds 'draft', its type is string, not Status. From the compiler's point of view, that variable could later become 'archived' or any other string. The assignment is rejected because Status only allows a limited set of values.
When you see this error, work through these questions in order:
- What is the target type expecting: a literal, a union, an enum member, or a branded string-like type?
- Why did my source value become a broad
stringinstead of a narrower literal type? - Can I narrow the value safely before assignment?
- Is my type model too strict, or is my runtime data too loose?
- Would a validation layer be more appropriate than a type assertion?
That sequence prevents the most common mistake: silencing the error with as before understanding where the type widened.
A few patterns cause most TS2322 and related assignability issues:
- Literal type widening in variables and object properties
- Passing plain strings into unions like
'sm' | 'md' | 'lg' - Using string enums or enum-like objects inconsistently
- Reading values from user input, URL params, local storage, or APIs
- Returning overly broad types from helper functions
Here is a simple mental model that holds up well: a narrow type can flow into a broad type, but a broad type cannot flow into a narrow type without proof.
type ButtonSize = 'sm' | 'md' | 'lg';
const a: ButtonSize = 'sm';
const b: string = a; // OK, narrow to broad
const c: string = 'sm';
const d: ButtonSize = c; // Error, broad to narrowThat is the core rule behind nearly every version of this error.
If you are still building confidence with related type-shaping techniques, our Interface vs Type in TypeScript: Current Best Practices guide is a useful companion. If the error appears while typing API data, How to Type API Responses in TypeScript Without Lying to the Compiler is the better next read.
Maintenance cycle
This section gives you a repeatable process you can use every time the error appears. The goal is not just to fix one line, but to identify the shape mismatch at the right layer.
1. Inspect the target type first
Start where the assignment fails. Hover the destination variable, property, or parameter and read the exact expected type.
type EmailStatus = 'draft' | 'sent';
function send(status: EmailStatus) {}
const input: string = 'draft';
send(input); // inspect EmailStatus firstAsk: is the target expecting a literal union, enum member, template-literal type, or some alias that resolves to a narrow string set?
2. Trace where the source widened
Most fixes come from finding the earlier point where a literal became string.
const status = 'draft'; // type is 'draft'
let current = 'draft'; // type is stringThe difference matters. const preserves the literal type in many cases, while let usually widens to string because the variable can change later.
The same issue often appears in objects:
const config = {
mode: 'dark'
};
// config.mode may be inferred as string in broader contextsIf the exact literal matters, you may need as const, a narrower annotation, or a better typed factory function.
3. Prefer narrowing over asserting
If a value really is dynamic, prove that it belongs to the narrower set before assignment.
type Role = 'admin' | 'user';
function isRole(value: string): value is Role {
return value === 'admin' || value === 'user';
}
const raw: string = getValue();
if (isRole(raw)) {
const role: Role = raw; // OK
}This is safer than writing raw as Role, which tells the compiler to trust you without runtime evidence.
4. Fix the source API if possible
If a helper function always returns one of a known set of strings, make that function return a union instead of string.
type Theme = 'light' | 'dark';
function getTheme(): Theme {
return Math.random() > 0.5 ? 'light' : 'dark';
}That single change often removes multiple downstream errors.
5. Use assertions sparingly and locally
Sometimes you have external guarantees that TypeScript cannot see. In those cases, use an assertion as close as possible to the boundary.
const fromTrustedConfig = process.env.APP_THEME as 'light' | 'dark';But treat this as a boundary decision, not a general-purpose fix. If the source is untrusted, validate it instead. For runtime schema approaches, see Zod vs io-ts vs Valibot vs Yup for TypeScript Runtime Validation.
6. Re-check utility and mapped types
Sometimes the error is indirect. A mapped type, indexed access, or generic constraint may be narrowing values more than you expected. If your project uses advanced transformations, it helps to review the source type rather than just the failing assignment. Our TypeScript Mapped Types Guide with Practical Patterns covers several of these patterns.
Signals that require updates
This topic stays relevant because the same error shows up across frameworks, new compiler versions, and changing team conventions. Here are the signals that mean your approach needs a refresh.
Your codebase is adding stricter unions
As projects mature, teams often replace broad strings with union types for props, state, event names, feature flags, and API status fields. That is usually a good move, but it exposes places where data is still typed as plain string.
Typical examples:
- React props like
variant,size, orintent - Backend status fields like
'pending' | 'processed' | 'failed' - URL or query parameters narrowed after parsing
- Configuration keys stored as string constants
You are migrating from JavaScript to TypeScript
During migration, many values start as any or string. Later, once you introduce proper domain types, assignments begin to fail. That is not a sign TypeScript is being difficult; it means your types are becoming more honest.
Common migration smell:
function setStatus(status: string) {
// ...
}
// later changed to
function setStatus(status: 'open' | 'closed') {
// ...
}All callers now need either narrower types or runtime checks.
You are reading more external data
Form values, JSON payloads, database text fields, search params, cookies, and environment variables all arrive as loose strings. As your type model becomes stricter, this error naturally appears more often at application boundaries.
That is a good sign to separate layers:
- boundary layer: parse and validate raw strings
- domain layer: use narrowed unions and trusted types
Your team is overusing as
If the same error keeps getting fixed with assertions, revisit the pattern. Repeated assertions often hide one upstream typing problem: a helper returning string, a config object inferred too broadly, or a missing type guard.
Compiler and config changes reveal hidden mismatches
Tighter project settings can surface errors that existed all along. If your team revises tsconfig, lint rules, or shared utility types, re-check assignment-heavy areas such as form handling, API adapters, and UI prop composition. For related setup issues, ESLint + TypeScript Flat Config Guide and How to Fix TypeScript Module Resolution Errors may help depending on where the breakage appears.
Common issues
This section covers the most frequent forms of the error and shows what usually fixes each one.
1. Literal type widening
This is the classic case.
type Direction = 'left' | 'right';
let dir = 'left';
const move: Direction = dir; // ErrorWhy it happens: dir is inferred as string because it is declared with let.
Better fixes:
const dir = 'left';
const move: Direction = dir; // OKlet dir: Direction = 'left';
const move: Direction = dir; // OKUse const when the value is fixed. Use an explicit union annotation when the variable can change, but only within allowed values.
2. Object property inference is too broad
type Variant = 'primary' | 'secondary';
const button = {
variant: 'primary'
};
const v: Variant = button.variant; // may error depending on inference contextFix options:
const button = {
variant: 'primary'
} as const;const button: { variant: Variant } = {
variant: 'primary'
};as const is useful when the whole object should be deeply readonly and preserve literals. An explicit annotation is better when the object is mutable or part of a public API.
3. Function parameters typed too loosely
type Tab = 'home' | 'settings';
function openTab(tab: Tab) {}
function handleTabChange(tab: string) {
openTab(tab); // Error
}Fix: narrow earlier or correct the parameter type.
function handleTabChange(tab: Tab) {
openTab(tab);
}If the input truly is any string, validate it:
function isTab(value: string): value is Tab {
return value === 'home' || value === 'settings';
}4. Arrays inferred as string[] instead of union arrays
type Status = 'draft' | 'read';
const values = ['draft', 'read'];
const statuses: Status[] = values; // often ErrorWhy: the array may infer to string[].
Fix options:
const values: Status[] = ['draft', 'read'];const values = ['draft', 'read'] as const;The second option gives you a readonly tuple, which is useful in lookup patterns and type derivation.
5. Enum confusion
String enums look similar to string unions, but they are not interchangeable.
enum EmailStatus {
Draft = 'draft',
Sent = 'sent'
}
const value: string = 'draft';
const status: EmailStatus = value; // ErrorWhy: a plain string is not automatically an enum member.
Fix options:
- Use the enum value directly:
EmailStatus.Draft - Validate a string before converting it
- Prefer string unions if you do not need enum runtime behavior
In many application-level cases, unions are simpler than enums and interact more naturally with external strings.
6. React props and DOM values
This error appears often in React because many DOM events expose values as plain strings.
type Filter = 'all' | 'active' | 'done';
function setFilter(filter: Filter) {}
function onChange(e: React.ChangeEvent<HTMLSelectElement>) {
setFilter(e.target.value); // Error
}Why: DOM input values are strings at runtime.
Fix: validate before assignment.
function isFilter(value: string): value is Filter {
return value === 'all' || value === 'active' || value === 'done';
}
function onChange(e: React.ChangeEvent<HTMLSelectElement>) {
const value = e.target.value;
if (isFilter(value)) setFilter(value);
}If you are working in framework codebases, this often overlaps with app-router params, query state, and fetched data. See Next.js + TypeScript Guide: App Router Patterns That Stay Type-Safe for related patterns.
7. API responses typed too optimistically
type ApiStatus = 'ok' | 'error';
const data = await fetchSomething();
const status: ApiStatus = data.status; // Error if status is stringCorrect approach: parse untrusted data and only then assign to your domain type.
This is one of the clearest cases where the compiler is protecting you from a real runtime risk.
8. Template literal and derived string types
The same principle applies to more advanced patterns.
type EventName = `user:${'create' | 'delete'}`;
const event: string = 'user:create';
const name: EventName = event; // ErrorEven if the current string matches, TypeScript needs either a narrower type or a proof step.
When to revisit
Use this section as a maintenance checklist. Revisit your approach to this error on a schedule, or any time the same fix keeps reappearing in reviews.
Revisit quarterly in active codebases
If your project adds new components, routes, endpoints, or shared packages regularly, check whether raw strings are still leaking into domain-level unions. This is especially useful in monorepos and shared UI libraries. If that sounds familiar, TypeScript Monorepo Setup Guide: pnpm, Project References, and Shared Types covers some of the structural causes.
Revisit when search intent or team pain changes
If your team keeps searching for the same error in the context of React forms, API typing, or path alias setups, update your local patterns and snippets around those workflows. The error itself is stable, but the places it appears most often can shift as your stack changes.
Practical checklist for the next occurrence
- Read the destination type carefully.
- Hover the source value and see where it widened to
string. - Replace broad helper return types with unions where appropriate.
- Add a type guard for dynamic values from users, URLs, storage, or APIs.
- Use
const, explicit annotations, oras constto preserve literals intentionally. - Avoid broad assertions unless you are at a trusted boundary.
- If the pattern repeats, redesign the source type instead of patching each assignment.
A good final rule is simple: do not ask TypeScript to trust a string that your runtime has not proved. If the value is truly constrained, model it narrowly at the source. If it is external or user-provided, validate it before it enters your domain layer. That habit fixes the error cleanly and keeps your types meaningful over time.
For adjacent debugging work, you may also want to keep these references nearby: TypeScript Path Aliases Guide for Vite, Next.js, Node, Jest, and ts-node and TanStack Query + TypeScript: Typing Queries, Mutations, and Infinite Lists. Both surface similar issues where broad external values meet narrow internal types.