Fix: TypeScript Type 'X | undefined' is not assignable to type 'X'
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 | undefinedCommon causes:
- Optional object properties. Properties marked with
?areT | undefined. - Array methods.
find,pop,shiftcan returnundefined. - Map and Set lookups.
.get()returnsT | undefined. - Function parameters. Optional parameters are
T | undefined. - DOM methods.
document.getElementById()returnsHTMLElement | 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
--strictNullChecksand the--strictumbrella flag. Before 2.0,nullandundefinedwere 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 treat0and""as missing. - TypeScript 4.1 (Nov 2020) added
--noUncheckedIndexedAccess. With it on,arr[0]andrecord["key"]becomeT | undefinedinstead ofT. 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
inoperator, and class field assignments. Patterns likeconst 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
satisfiesoperator, 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: bundlerand reworked decorators. Strict null behavior is unchanged, but stricter resolution can surface previously hiddenT | undefineddeclarations from.d.tsfiles that were silently treated asanyundernoderesolution. - TypeScript 5.2 (Aug 2023) stabilized explicit resource management (
using). The implicitDisposable | undefinedplumbing relies on strict null checks; without them, theusingdesugaring 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 localconstto keep the narrowing. - TypeScript 5.5 (Jun 2024) introduced inferred type predicates for
Array.prototype.filter.users.filter(u => u !== undefined)now returnsUser[]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 bothnullandundefinedin 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 UserFixed — 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 hereFixed — 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 | undefinedFix 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>; // stringThe 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Deno PermissionDenied — Missing --allow-read, --allow-net, and Other Flags
How to fix Deno PermissionDenied (NotCapable in Deno 2) errors — the right permission flags, path-scoped permissions, deno.json permission sets, and the Deno.permissions API.
Fix: Fastify Not Working — 404, Plugin Encapsulation, and Schema Validation Errors
How to fix Fastify issues — route 404 from plugin encapsulation, reply already sent, FST_ERR_VALIDATION, request.body undefined, @fastify/cors, hooks not running, and TypeScript type inference.
Fix: Vinxi Not Working — Dev Server Not Starting, Routes Not Matching, or Build Failing
How to fix Vinxi server framework issues — app configuration, routers, server functions, middleware, static assets, and deployment to different platforms.
Fix: Clack Not Working — Prompts Not Displaying, Spinners Stuck, or Cancel Not Handled
How to fix @clack/prompts issues — interactive CLI prompts, spinners, multi-select, confirm dialogs, grouped tasks, cancellation handling, and building CLI tools with beautiful output.