TypeScriptnull & undefined

null & undefined

null and undefined are the most common source of runtime errors in JavaScript. The infamous "Cannot read properties of undefined" and "Cannot read properties of null" errors account for a massive share of production bugs. TypeScript's strict null checking system makes these invisible landmines visible — catching them at compile time, long before they reach your users.

What Are null and undefined?

JavaScript has two distinct "nothing" values, and they mean subtly different things:

Value

Meaning

When You See It

undefined

A variable was declared but never assigned

Uninitialized variables, missing object properties, function parameters with no argument passed, function return with no return statement

null

An intentional absence of value

Explicitly set by code: "this thing exists but has no value right now"

TS
// undefined — never assigned
let x: number;
console.log(x);  // undefined at runtime

// undefined — missing property
const obj = { name: 'Alice' };
console.log((obj as any).age);  // undefined at runtime

// null — intentional empty value
let selectedUser: User | null = null;
// Later: selectedUser = await loadUser(id);

// typeof both
typeof undefined  // "undefined"
typeof null       // "object"  ← infamous JavaScript quirk
strictNullChecks: The Critical Setting

TypeScript's behaviour around null and undefined is controlled by the strictNullChecks compiler option (enabled automatically by strict: true).

Setting

null and undefined behaviour

strictNullChecks: false (not recommended)

null and undefined are assignable to every type — string, number, User, etc. Basically turns off null safety.

strictNullChecks: true (always use this)

null and undefined are their own separate types. To allow null, you must use an explicit union type like string | null.

TS
// strictNullChecks: true (correct setup)

let name: string = 'Alice';
name = null;       // Error: Type 'null' is not assignable to type 'string'
name = undefined;  // Error: Type 'undefined' is not assignable to type 'string'

// To allow null — make it explicit
let nullable: string | null = 'Alice';
nullable = null;   // OK

// To allow undefined — also explicit
let optional: string | undefined = 'Alice';
optional = undefined;   // OK

// To allow both
let either: string | null | undefined;
Warning
Without strictNullChecks, TypeScript loses most of its ability to prevent null pointer errors. Always include strict: true (or at minimum strictNullChecks: true) in your tsconfig.json.
Handling null and undefined: Core Patterns
Pattern 1: if Check (Explicit Narrowing)

TS
function greet(name: string | null): string {
  if (name === null) {
    return 'Hello, stranger!';
  }
  // TypeScript knows name is string here (null was ruled out)
  return `Hello, ${name}!`;
}

// Truthy check works too (but be careful with empty string)
function greet2(name: string | null): string {
  if (name) {
    return `Hello, ${name}!`;  // string
  }
  return 'Hello, stranger!';
}
Pattern 2: Optional Chaining (?.)

Optional chaining (?.) short-circuits to undefined when the left side is null or undefined, instead of throwing:

TS
interface User {
  name: string;
  address?: {
    street: string;
    city: string;
  };
}

const user: User | null = getUser(id);

// Without optional chaining — verbose and error-prone
if (user !== null && user.address !== undefined) {
  console.log(user.address.city);
}

// With optional chaining — clean and safe
console.log(user?.address?.city);  // string | undefined (no crash if null/undefined)

// Works with method calls too
const upper = user?.name.toUpperCase();  // string | undefined

// Works with array access
const first = arr?.[0];  // T | undefined
Pattern 3: Nullish Coalescing (??)

The nullish coalescing operator (??) returns the right-hand side when the left is null or undefined, but not for other falsy values like 0 or "". This is a crucial distinction from ||.

TS
const name: string | null = getUserName();

// ?? — only falls back for null and undefined
const displayName = name ?? 'Anonymous';
// 'Anonymous' if name is null or undefined
// name itself if name is '' or any other string (including empty!)

// Compare with ||
const displayName2 = name || 'Anonymous';
// 'Anonymous' if name is null, undefined, '', 0, false, NaN
// This is often wrong — empty string is a valid name

// Practical example
const timeout = userConfig.timeout ?? 5000;
// Uses 5000 only if timeout is not set — not if it is 0
// With ||, a 0 timeout would incorrectly fall back to 5000
Tip
Use ?? instead of || for default values whenever 0, false, or "" are meaningful values that should NOT trigger the fallback. || treats all falsy values the same, which is almost always wrong for default-value logic.
Pattern 4: Nullish Assignment (??=)

TS
// Assign only if the value is currently null or undefined
let config: Record<string, unknown> = {};

config.timeout ??= 5000;        // sets to 5000 (was undefined)
config.timeout ??= 10000;       // does NOT change — was already 5000

// Equivalent to: if (config.timeout === null || config.timeout === undefined)
//                  config.timeout = 5000;
Non-Null Assertion Operator (!)

The non-null assertion operator (!) tells TypeScript "I guarantee this is not null or undefined." It removes null and undefined from the type without any runtime check.

TS
// document.getElementById returns HTMLElement | null
const el = document.getElementById('root');

// Without assertion — TypeScript complains
el.innerHTML = 'Hello';  // Error: Object is possibly 'null'

// With non-null assertion — you take responsibility
const el2 = document.getElementById('root')!;
el2.innerHTML = 'Hello';  // OK — TypeScript trusts you

// Equivalent to:
const el3 = document.getElementById('root') as HTMLElement;
Warning
The ! operator tells TypeScript to skip the null check — it does NOT add a runtime check. If the value is actually null or undefined, you still get a runtime error. Use it sparingly and only when you are certain.
undefined in Function Parameters

Optional parameters in functions are automatically T | undefined. You can also explicitly accept undefined to distinguish "not passed" from "passed as undefined":

TS
// Optional parameter — callers can omit it
function greet(name?: string): string {
  // name is string | undefined inside the function
  return name ? `Hello, ${name}!` : 'Hello, stranger!';
}

greet('Alice');   // OK
greet();          // OK — name is undefined

// Default parameter — best practice when undefined means "use a default"
function greetWithDefault(name: string = 'stranger'): string {
  // name is always string here — the default handles undefined
  return `Hello, ${name}!`;
}

greetWithDefault();           // 'Hello, stranger!'
greetWithDefault(undefined);  // 'Hello, stranger!' — undefined triggers the default
greetWithDefault('Alice');    // 'Hello, Alice!'
undefined in Object Properties

TS
interface User {
  id: number;
  name: string;
  nickname?: string;        // optional: string | undefined
  middleName: string | undefined;  // required but can be undefined
}

// Optional vs explicitly-undefined — they look similar but differ:
const u1: User = { id: 1, name: 'Alice', nickname: undefined };      // OK
const u2: User = { id: 1, name: 'Alice' };                           // OK — nickname omitted
const u3: User = { id: 1, name: 'Alice', middleName: undefined };    // OK
const u4: User = { id: 1, name: 'Alice' };
// Error: Property 'middleName' is missing (it is required, just allows undefined as value)

// Checking for optional property presence
function showNickname(user: User) {
  if ('nickname' in user && user.nickname !== undefined) {
    console.log(`Known as: ${user.nickname}`);
  }
}
Returning null vs undefined from Functions

As a convention: return null when a value was explicitly not found or does not apply, and use undefined (or omit the return) for functions that simply do not produce a value.

TS
// Returns null: indicates "not found" — explicit absence
function findUser(id: number): User | null {
  const user = database.find((u) => u.id === id);
  return user ?? null;
}

// Returns undefined implicitly: "no value produced"
function printMessage(msg: string): void {
  console.log(msg);
  // implicitly returns undefined
}

// Common convention: null for "nothing found", undefined for "not applicable"
interface ApiResponse<T> {
  data: T | null;       // null: server found nothing
  error: string | null; // null: no error
}
Discriminated Unions for Better Null Handling

Instead of nullable types, consider discriminated unions for more expressive state:

TS
// Instead of: User | null with separate loading/error state...
type FetchState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

type UserFetchState = FetchState<User>;

function render(state: UserFetchState): string {
  switch (state.status) {
    case 'idle':    return 'Click to load...';
    case 'loading': return 'Loading...';
    case 'success': return `Hello, ${state.data.name}`;  // data: User guaranteed
    case 'error':   return `Error: ${state.error.message}`;
  }
}

// state.data is only accessible when status === 'success'
// TypeScript enforces this — no accidental null access
Success
Discriminated unions eliminate entire classes of null errors by making each state explicit. The compiler ensures you handle every case and only access data when it is actually available.
Null Checks in Practice: Real Patterns
Fetching from an API

TS
async function getUser(id: number): Promise<User | null> {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) return null;
    return res.json() as Promise<User>;
  } catch {
    return null;
  }
}

async function displayUser(id: number) {
  const user = await getUser(id);

  // TypeScript forces you to handle the null case
  if (!user) {
    console.log('User not found');
    return;
  }

  // user is User here — TypeScript is certain
  console.log(`${user.name} (${user.email})`);
}
Optional Chaining with Nullish Coalescing

TS
interface Config {
  server?: {
    host?: string;
    port?: number;
  };
}

function getPort(config: Config): number {
  // Deeply nested optional access with a default
  return config.server?.port ?? 3000;
}

// This replaces the old verbose pattern:
function getPortVerbose(config: Config): number {
  if (config.server !== undefined && config.server.port !== undefined) {
    return config.server.port;
  }
  return 3000;
}
Array Filter to Remove Nulls

TS
const userIds = [1, 2, 3, 4];
const maybeUsers: (User | null)[] = await Promise.all(
  userIds.map((id) => getUser(id)),
);

// Type predicate removes null from the array type
const users: User[] = maybeUsers.filter((u): u is User => u !== null);

// Without the predicate, TypeScript would infer (User | null)[] after filter
// The predicate tells TypeScript: if the callback returns true, the element is User
noUncheckedIndexedAccess

The noUncheckedIndexedAccess compiler option (not part of strict) adds | undefined to array index access, since TypeScript cannot guarantee an index is in bounds at compile time:

TS
// Without noUncheckedIndexedAccess
const arr = [1, 2, 3];
const first: number = arr[0];   // inferred as number (could be undefined if empty)
const outOfBounds: number = arr[99]; // number (but actually undefined at runtime!)

// With noUncheckedIndexedAccess: true
const arr2 = [1, 2, 3];
const first2: number | undefined = arr2[0];  // TypeScript adds | undefined
if (first2 !== undefined) {
  console.log(first2 * 2);  // OK — narrowed to number
}
Note
noUncheckedIndexedAccess is stricter than the default. It can produce many extra | undefined checks in array-heavy code. Enable it gradually in established codebases, or adopt it from the start in new projects.
Common Mistakes and Fixes

Mistake

Fix

Using || for defaults when 0 or "" is valid

Use ?? instead

Using ! on every nullable value

Use if checks or ?. and ?? to handle the nullability

Not handling undefined from array access

Enable noUncheckedIndexedAccess or check before use

Returning undefined instead of null for "not found"

Use null — it signals intentional absence

Allowing both null and undefined for the same field

Pick one — use null for domain absence, undefined for optional

Summary
  • Enable strict: true (which includes strictNullChecks) — always, no exceptions

  • null means "intentionally empty"; undefined means "not set / optional"

  • Use union types (string | null) to explicitly allow null or undefined

  • Optional chaining (?.) safely traverses nullable property chains

  • Nullish coalescing (??) provides defaults only for null/undefined, not all falsy values

  • The non-null assertion (!) removes null from the type without a runtime check — use sparingly

  • Default parameters are the cleanest way to handle undefined function arguments

  • Discriminated unions provide richer state modelling than nullable types alone

  • Type predicates in filter() remove null from array types safely