Skip to content

Fix: TypeScript Type 'X | undefined' is not assignable to type 'X'

FixDevs · (Updated: )

Part of:  JavaScript & TypeScript Errors

Quick Answer

How to fix TypeScript strict null checks error Type X undefined is not assignable caused by optional values, nullable types, missing guards, and strictNullChecks.

The Error

You compile TypeScript and get:

error TS2322: Type 'string | undefined' is not assignable to type 'string'.
  Type 'undefined' is not assignable to type 'string'.

Or variations:

error TS2532: Object is possibly 'undefined'.
error TS2322: Type 'number | null' is not assignable to type 'number'.
  Type 'null' is not assignable to type 'number'.
error TS18048: 'user' is possibly 'undefined'.
error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'.

TypeScript’s strict null checking is telling you that a value might be undefined or null, but the target type does not accept those values. You need to narrow the type before using it.

Why This Happens

When strictNullChecks is enabled in tsconfig.json (or strict: true, which includes it), TypeScript tracks null and undefined as distinct types. A string type only accepts strings — not undefined or null. Without strict null checks, every variable can secretly be null or undefined and TypeScript silently lets you call methods on them. With strict null checks, the compiler forces you to prove the value exists before you touch it.

The error is structural, not stylistic. TypeScript is performing flow-sensitive type narrowing on your code: it knows the variable’s declared type, it knows the code paths that may have changed it, and it surfaces every gap where the narrowed type still includes undefined or null. A successful fix changes the narrowed type by adding a check, a default, or a stronger annotation — silencing the message with any or // @ts-ignore does not change the runtime risk.

Most modern TypeScript code bases enable strict by default because tsc --init since TypeScript 4.0 (Aug 2020) generates strict: true in the starter tsconfig.json. Newer frameworks such as Next.js, Remix, Astro, SvelteKit, Vue’s create-vue, and Vite’s TS template all ship strict: true. If you inherited a code base where this error suddenly started appearing across hundreds of files, it is almost always because someone flipped strict or strictNullChecks from false to true.

Operations that return possibly-undefined values:

// Optional properties are T | undefined
interface User {
  name: string;
  email?: string;  // string | undefined
}

// Array.find() returns T | undefined
const user = users.find(u => u.id === 1);  // User | undefined

// Map.get() returns T | undefined
const value = myMap.get("key");  // V | undefined

// Optional chaining returns T | undefined
const city = user?.address?.city;  // string | undefined

// Object property access with index signature
const item = record["key"];  // T | undefined

Common causes:

  • Optional object properties. Properties marked with ? are T | undefined.
  • Array methods. find, pop, shift can return undefined.
  • Map and Set lookups. .get() returns T | undefined.
  • Function parameters. Optional parameters are T | undefined.
  • DOM methods. document.getElementById() returns HTMLElement | null.
  • External API data. API responses may have nullable fields.

Version History That Changes the Failure Mode

The exact behavior, error code, and recommended fix have shifted across TypeScript releases. Knowing which version you are on tells you which fixes are even available.

  • TypeScript 2.0 (Sept 2016) introduced --strictNullChecks and the --strict umbrella flag. Before 2.0, null and undefined were assignable to any type and this error did not exist. Every code base that still pins TS < 2.0 is fundamentally unsafe.
  • TypeScript 3.7 (Nov 2019) shipped optional chaining (?.) and nullish coalescing (??). Most of Fix 2 and Fix 4 below only compile cleanly here. Older code bases use verbose && chains and || defaults that silently treat 0 and "" as missing.
  • TypeScript 4.1 (Nov 2020) added --noUncheckedIndexedAccess. With it on, arr[0] and record["key"] become T | undefined instead of T. This flag dramatically increases the number of TS18048 / TS2532 errors you see, especially in code that walks arrays by index.
  • TypeScript 4.4 (Aug 2021) extended control-flow analysis to aliased conditions, the in operator, and class field assignments. Patterns like const isDefined = x !== undefined; if (isDefined) { x.foo; } started narrowing correctly. If you are on an older release, you must inline the check.
  • TypeScript 4.9 (Nov 2022) introduced the satisfies operator, which lets you assert that a value matches a type without widening it. It is useful when you want to keep the literal type narrow while still catching nullability mismatches.
  • TypeScript 5.0 (Mar 2023) added --moduleResolution: bundler and reworked decorators. Strict null behavior is unchanged, but stricter resolution can surface previously hidden T | undefined declarations from .d.ts files that were silently treated as any under node resolution.
  • TypeScript 5.2 (Aug 2023) stabilized explicit resource management (using). The implicit Disposable | undefined plumbing relies on strict null checks; without them, the using desugaring fails to catch missing disposers.
  • TypeScript 5.4 (Mar 2024) added NoInfer<T> and refined narrowing inside closures so that values narrowed at the outer scope remain narrowed inside callbacks more often. Before 5.4, you frequently had to re-check or re-assign to a local const to keep the narrowing.
  • TypeScript 5.5 (Jun 2024) introduced inferred type predicates for Array.prototype.filter. users.filter(u => u !== undefined) now returns User[] automatically. Pre-5.5 you had to write (u): u is User => u !== undefined (see Fix 5 below).

If your package.json pins TypeScript below 4.4, upgrade before reaching for elaborate workarounds — the compiler simply cannot narrow patterns that work transparently in 4.4+.

Fix 1: Use Type Guards (if/else)

The most reliable approach. Check for undefined/null before using the value:

Broken:

function greet(user: User) {
  const email: string = user.email;  // Error: string | undefined not assignable to string
  sendEmail(email);
}

Fixed — check with if statement:

function greet(user: User) {
  if (user.email !== undefined) {
    // TypeScript knows email is string here (narrowed)
    sendEmail(user.email);
  }
}

For null checks:

const element = document.getElementById("app");
if (element !== null) {
  element.textContent = "Hello";  // TypeScript knows element is HTMLElement
}

Combining null and undefined checks:

if (value != null) {
  // Checks both null and undefined (loose equality)
  process(value);  // TypeScript narrows correctly
}

Pro Tip: Use != null (loose equality with two =) to check for both null and undefined in one condition. This is one of the rare cases where loose equality is preferred over strict equality in TypeScript.

Fix 2: Use the Nullish Coalescing Operator (??)

Provide a default value when the value might be null or undefined:

interface Config {
  timeout?: number;
  retries?: number;
  baseUrl?: string;
}

function createClient(config: Config) {
  const timeout: number = config.timeout ?? 5000;
  const retries: number = config.retries ?? 3;
  const baseUrl: string = config.baseUrl ?? "https://api.example.com";
}

?? vs ||:

// ?? only falls back for null/undefined
const count = config.count ?? 10;
// If config.count is 0, count is 0 (correct!)
// If config.count is undefined, count is 10

// || falls back for ANY falsy value (0, "", false, null, undefined)
const count = config.count || 10;
// If config.count is 0, count is 10 (probably wrong!)

Use ?? when 0, "", or false are valid values.

Fix 3: Use Non-Null Assertion (!)

When you are certain the value is not null/undefined:

// You know the element exists because your HTML has it
const app = document.getElementById("app")!;
app.textContent = "Hello";

// After a check in a different scope
const user = users.find(u => u.id === id);
// You know the user exists because of business logic
processUser(user!);

Warning: The ! operator tells TypeScript to trust you. If the value actually is null/undefined at runtime, your code will crash. Only use it when you are genuinely certain.

Better alternatives when possible:

// Instead of:
const element = document.getElementById("app")!;

// Use a guard with an error:
const element = document.getElementById("app");
if (!element) throw new Error("App element not found");
// element is now HTMLElement (narrowed)

Common Mistake: Overusing the non-null assertion operator (!) to silence TypeScript errors. Every ! is a place where your code can crash at runtime. Use it sparingly and only when you can guarantee the value exists.

Fix 4: Use Optional Chaining (?.)

Access deeply nested optional properties safely:

interface Order {
  customer?: {
    address?: {
      city?: string;
    };
  };
}

// Broken — any level could be undefined
const city: string = order.customer.address.city;  // Multiple errors!

// Fixed — optional chaining
const city: string | undefined = order.customer?.address?.city;

// With a default value
const city: string = order.customer?.address?.city ?? "Unknown";

With method calls:

const length: number = user.getName?.().length ?? 0;

With array access:

const firstItem: string | undefined = items?.[0];

Fix 5: Fix Array Methods

find, pop, shift, and index access return possibly-undefined:

Broken:

const users: User[] = getUsers();
const admin: User = users.find(u => u.role === "admin");
// Error: User | undefined is not assignable to User

Fixed — type guard:

const admin = users.find(u => u.role === "admin");
if (admin) {
  processAdmin(admin);  // admin is User here
}

Fixed — with assertion and error:

const admin = users.find(u => u.role === "admin");
if (!admin) {
  throw new Error("No admin user found");
}
processAdmin(admin);  // admin is User here

Fixed — filter and assert the type:

// filter with type predicate
const admins: User[] = users.filter((u): u is User => u.role === "admin");
const firstAdmin: User | undefined = admins[0];

For array index access:

const items: string[] = ["a", "b", "c"];

// With noUncheckedIndexedAccess (tsconfig), items[0] is string | undefined
const first = items[0];  // string | undefined

// Fixed
if (items.length > 0) {
  const first = items[0]!;  // Safe because we checked length
}

Fix 6: Fix Function Parameter Types

Optional parameters and return types:

// Optional parameter
function greet(name?: string) {
  // name is string | undefined
  const upper: string = name.toUpperCase();  // Error!

  // Fixed
  const upper: string = (name ?? "World").toUpperCase();
}

// Return type might be undefined
function findUser(id: number): User | undefined {
  return users.find(u => u.id === id);
}

// Caller must handle undefined
const user = findUser(1);
if (user) {
  console.log(user.name);
}

Overloads for different return types:

function findUser(id: number, required: true): User;
function findUser(id: number, required?: false): User | undefined;
function findUser(id: number, required = false): User | undefined {
  const user = users.find(u => u.id === id);
  if (required && !user) throw new Error(`User ${id} not found`);
  return user;
}

const user = findUser(1, true);   // Type is User (guaranteed)
const maybe = findUser(1);         // Type is User | undefined

Fix 7: Fix Map and Record Types

Map.get() and record indexing return possibly-undefined:

const cache = new Map<string, User>();

// Map.get returns V | undefined
const user: User = cache.get("alice");  // Error!

// Fixed — check with has() or guard
if (cache.has("alice")) {
  const user = cache.get("alice")!;  // Safe after has() check
}

// Fixed — guard
const user = cache.get("alice");
if (user) {
  process(user);
}

Record with index signature:

const config: Record<string, string> = loadConfig();

const value: string = config["key"];  // string (Record assumes keys exist)
// BUT with noUncheckedIndexedAccess: string | undefined

// Safer approach
const value = config["key"];
if (value !== undefined) {
  use(value);
}

Fix 8: Create Type-Safe Helper Functions

Build reusable utilities for common patterns:

// Assert non-null with a descriptive error
function assertDefined<T>(value: T | undefined | null, message: string): T {
  if (value === undefined || value === null) {
    throw new Error(message);
  }
  return value;
}

const user = assertDefined(
  users.find(u => u.id === id),
  `User with id ${id} not found`
);
// user is User (guaranteed)

// Type-safe Map wrapper
class TypedMap<K, V> {
  private map = new Map<K, V>();

  get(key: K): V | undefined {
    return this.map.get(key);
  }

  getOrThrow(key: K): V {
    const value = this.map.get(key);
    if (value === undefined) throw new Error(`Key not found: ${String(key)}`);
    return value;
  }

  getOrDefault(key: K, defaultValue: V): V {
    return this.map.get(key) ?? defaultValue;
  }
}

Still Not Working?

Check your tsconfig.json settings:

{
  "compilerOptions": {
    "strict": true,
    "strictNullChecks": true
  }
}

If strictNullChecks is false, these errors will not appear — but you lose important safety checks. Keeping it enabled is recommended.

Use Partial<T> for objects with all optional properties:

type Config = Partial<FullConfig>;
// All properties become optional (T | undefined)

Use Required<T> to make all properties required:

type StrictConfig = Required<Config>;
// All properties become required (no undefined)

Use NonNullable<T> to strip null/undefined:

type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>;  // string

The error vanishes after you assign to a variable. TypeScript narrows on the narrowed expression, not on the original property. If you copy user.email into a local const email while it is still possibly undefined, then check if (email) { ... }, narrowing works. But if you re-access user.email after if (user.email) and then await something(), the narrowing is dropped because the awaited call could have mutated user. Cache to a local first.

Discriminated union members appear to be undefined. If you have type Result = { ok: true; value: T } | { ok: false; error: E }, accessing result.value errors with TS18048 because the ok: false member does not have value. Narrow with if (result.ok) before touching result.value. This is not a strictNullChecks issue — it is the same narrowing engine refusing to assume the discriminant.

React refs and useRef trip this constantly. useRef<HTMLDivElement>(null) gives you RefObject<HTMLDivElement> whose .current is HTMLDivElement | null. You must check if (ref.current) { ... } before calling .focus() or .scrollIntoView(). Switching to useRef<HTMLDivElement>(null!) silences the error but lies — use a guard.

Third-party .d.ts files declare a value as T when it is actually T | undefined. Library authors sometimes lie. If you see runtime crashes despite passing strict null checks, suspect the type definitions. Wrap the call in your own helper that returns T | undefined, then narrow.

For TypeScript property errors, see Fix: TypeScript Property does not exist on type. For the closely related possibly-undefined error, see Fix: TypeScript Object is possibly undefined. For module resolution errors, see Fix: TypeScript Cannot find module. For general type assignment errors, see Fix: TypeScript type is not assignable.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles