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" |
// 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 quirkstrictNullChecks: 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. |
// 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;
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)
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:
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 | undefinedPattern 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 ||.
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
?? 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 (??=)
// 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.
// 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;! 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":
// 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
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.
// 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:
// 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 accessNull Checks in Practice: Real Patterns
Fetching from an API
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
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
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:
// 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
}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