Fix TS2322: Type is not assignable to type (TypeScript 2026)

TS2322: Type 'X' is not assignable to type 'Y' is the number one TypeScript compiler error. It shows up when you try to assign a value whose inferred type does not match the expected type. Common triggers include string vs undefined mismatches, React refs, enums, and object shape mismatches. Here is the complete 2026 fix guide with real code.

Why TS2322 happens

TypeScript computes the type of every value at compile time. When you assign to a variable, function parameter, or return type, the compiler checks that the value’s type is compatible with the expected type. If not compatible, it emits TS2322 with both types spelled out. Fix by narrowing the value’s type, widening the target type, or asserting when you know better than the compiler.

Fix 1: string vs undefined (most common)

// WRONG: search param can be undefined
const search: string = router.query.search;
// TS2322: Type 'string | undefined' is not assignable to type 'string'.

// FIX 1: narrow with a default
const search: string = router.query.search ?? "";

// FIX 2: narrow with type guard
if (typeof router.query.search === "string") {
  const search: string = router.query.search;
}

// FIX 3: widen the target type
const search: string | undefined = router.query.search;

// FIX 4: assert (only when you are certain it exists)
const search: string = router.query.search!;   // non-null assertion

Fix 2: React ref types

import { useRef } from "react";

// WRONG: ref.current can be null before mount
const inputRef = useRef<HTMLInputElement>(null);
inputRef.current.focus();
// TS2531 or TS18047: Object is possibly 'null'.

// FIX: guard before use
inputRef.current?.focus();

// Or narrow with useEffect
useEffect(() => {
  if (inputRef.current) {
    inputRef.current.focus();   // narrowed to HTMLInputElement
  }
}, []);

Fix 3: enum vs literal string

enum Status {
  Pending = "pending",
  Active = "active",
  Closed = "closed",
}

// WRONG: string literal is not a Status enum value at compile time
const s: Status = "active";
// TS2322: Type '"active"' is not assignable to type 'Status'.

// FIX 1: use the enum
const s: Status = Status.Active;

// FIX 2: use a union of string literals instead of enum (recommended in 2026)
type Status = "pending" | "active" | "closed";
const s: Status = "active";   // works

Fix 4: object shape mismatch

type User = { id: number; email: string; role: "admin" | "member" };

// WRONG: missing 'role' property
const u: User = { id: 1, email: "[email protected]" };
// TS2322: Type '{ id: number; email: string; }' is not assignable to type 'User'. Property 'role' is missing.

// FIX 1: add the missing property
const u: User = { id: 1, email: "[email protected]", role: "member" };

// FIX 2: make role optional in the type
type User = { id: number; email: string; role?: "admin" | "member" };

// FIX 3: use Partial when building progressively
const draft: Partial<User> = { id: 1, email: "[email protected]" };

Debugging checklist

  • Read the full error text. TS2322 always spells out both source and target types explicitly.
  • Hover the value in VS Code to see its inferred type. Compare to the target type.
  • Check tsconfig.json for strict and strictNullChecks. Both add null/undefined to inferred types.
  • If the value comes from an external API, wrap the fetch in a runtime validator (Zod, io-ts) so types match reality.

Real-world example: form input handler

type Form = { name: string; email: string; age: number };

function handleChange(
  field: keyof Form,
  value: string
): Partial<Form> {
  // WRONG: age is number, value is string
  // return { [field]: value };
  // TS2322: Type 'string' is not assignable to type 'string | number'.

  // FIX: narrow by field name
  if (field === "age") {
    const n = Number(value);
    if (Number.isNaN(n)) throw new Error("age must be a number");
    return { age: n };
  }
  return { [field]: value };   // string fields
}

Common mistakes when fixing TS2322

  • Reaching for any or as unknown as. These bypass the compiler and hide bugs. Use only as last resort with a comment explaining why.
  • Widening types too much. Making everything optional or | undefined shifts the null check burden downstream. Narrow at the boundary instead.
  • Using non-null assertion (!) as a habit. Every ! is a runtime bug waiting to happen. Guard properly with ? or type guards.
  • Disabling strictNullChecks to make errors go away. You lose all null safety. Fix the actual types instead.

Debugging TypeScript errors systematically

TypeScript errors follow a predictable pattern once you understand the compiler’s logic. Before jumping to fixes, run through this diagnostic checklist.

  1. Read the FULL error message. The compiler tells you both the actual type it sees and the expected type. Match them side by side to spot the mismatch.
  2. Check strictNullChecks and strict mode. Many errors trace back to strict: true in tsconfig.json. If you inherited a codebase with strict mode on, expect more errors than in loose mode.
  3. Look at inferred types with hover. VS Code shows inferred types on hover. Compare what TypeScript sees vs what you assumed.
  4. Use // @ts-expect-error strategically. When you know better than the compiler temporarily, mark the line. TypeScript flags the annotation when the underlying issue is fixed.
  5. Isolate the failing line in a minimal reproducer. A standalone .ts file with hardcoded inputs often surfaces the root cause faster than debugging in place.

Common TypeScript configuration issues

Many “why does this not compile” issues trace back to tsconfig.json, not your code.

  • strict flag toggles many checks at once. strict: true enables strictNullChecks, noImplicitAny, strictFunctionTypes, and more. Turning on strict without preparing your code produces hundreds of errors.
  • Version mismatch between global and project TypeScript. Different TypeScript versions produce different errors. Pin the version in package.json for consistency.
  • skipLibCheck can hide real issues. Setting skipLibCheck: true speeds up compilation but hides type mismatches in dependencies. Fine for CI but keep it off during development.
  • Path aliases and moduleResolution mismatches. The paths config in tsconfig only works with the right moduleResolution setting. Match “bundler” for Vite, “node16” for Node.
  • Ambient declarations vs modules. Files with imports are modules; files without imports are ambient scripts. Some errors happen because a file is treated as one when you expected the other.

Production TypeScript best practices

Once you get past errors and ship code, keep these habits to prevent recurring problems.

  • Enable strict mode from day 1 on new projects. Migrating to strict later is 10x more work.
  • Never use any without a good reason. unknown is safer and forces you to narrow before use.
  • Avoid type assertions (as) except at API boundaries. Assertions bypass the type system. Use them sparingly at edges (JSON parsing, DOM, external APIs).
  • Prefer type over interface for new code. Type aliases work everywhere interfaces do, plus support unions, intersections, and mapped types.
  • Test type-level logic. Tools like expect-type or tsd let you write tests for your type definitions.

Learning resources for TypeScript

Beyond fixing the current error, deeper TypeScript knowledge pays dividends for years. Recommended starting points:

  • Official Handbook. typescriptlang.org/docs/handbook. The canonical reference, well-written and up-to-date.
  • Total TypeScript by Matt Pocock. Free tutorials and paid courses. Best for going from junior to advanced TypeScript.
  • Type Challenges GitHub repo. Interactive problems that build type-level programming skills.
  • TypeScript Deep Dive. Free online book, especially good for the “why” behind features.
  • Try in the playground. typescriptlang.org/play lets you experiment with types and see errors immediately.

Frequently Asked Questions

Is TS2322 always a real bug?

Usually yes. The compiler sees a real mismatch. In rare cases where you have runtime knowledge the compiler cannot see, use a type guard or narrow assertion. Never use any to silence it.

When should I use as assertions?

Only when you have runtime evidence that the compiler cannot see. Every as is a promise to the compiler. If wrong, you introduce a runtime bug that TypeScript would have caught.

Enum or union of literals?

In 2026, prefer unions of string literals. Enums have runtime cost and less flexible narrowing. Only use enums for compatibility with existing code or when you need reverse mapping.

Does turning off strict mode fix TS2322?

It hides the error but not the bug. Disabling strict removes null and undefined from inferred types. Your runtime crashes will still happen, just without warning. Never disable strict on a modern project.

Why does the API response cause TS2322?

Fetch responses are typed as unknown or you cast to a shape that may not match reality. Use a runtime validator like Zod at the boundary so types match what actually arrives.

Leave a Comment