How to Fix "Type 'string' is not assignable to type" Errors in TypeScript
assignabilitytypescript errorsliteralstype narrowingunionsdebugging

How to Fix "Type 'string' is not assignable to type" Errors in TypeScript

TTypeScript Page Editorial
2026-06-13
10 min read

A practical guide to fixing TypeScript's “Type 'string' is not assignable to type” errors with narrowing, unions, literals, and enums.

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; // Error

Even 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:

  1. What is the target type expecting: a literal, a union, an enum member, or a branded string-like type?
  2. Why did my source value become a broad string instead of a narrower literal type?
  3. Can I narrow the value safely before assignment?
  4. Is my type model too strict, or is my runtime data too loose?
  5. 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 narrow

That 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 first

Ask: 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 string

The 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 contexts

If 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, or intent
  • 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; // Error

Why it happens: dir is inferred as string because it is declared with let.

Better fixes:

const dir = 'left';
const move: Direction = dir; // OK
let dir: Direction = 'left';
const move: Direction = dir; // OK

Use 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 context

Fix 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 Error

Why: 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; // Error

Why: 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 string

Correct 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; // Error

Even 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

  1. Read the destination type carefully.
  2. Hover the source value and see where it widened to string.
  3. Replace broad helper return types with unions where appropriate.
  4. Add a type guard for dynamic values from users, URLs, storage, or APIs.
  5. Use const, explicit annotations, or as const to preserve literals intentionally.
  6. Avoid broad assertions unless you are at a trusted boundary.
  7. 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.

Related Topics

#assignability#typescript errors#literals#type narrowing#unions#debugging
T

TypeScript Page Editorial

Senior SEO Editor

Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.

2026-06-17T10:20:34.840Z