TypeScript Null Safety
A reference for handling null and undefined safely in TypeScript projects.
TL;DR
- 01Use optional chaining (?.) to safely access nested properties on nullable values.
- 02Use nullish coalescing (??) to supply default values for null or undefined.
- 03Apply type guards to narrow a union type before accessing its properties.
Tips
- 01Enable strictNullChecks in tsconfig.json to catch null safety issues at compile time.
Warnings
- 01Non-null assertions (!) bypass type checking — use them only when you're certain the value is not null.
Optional Chaining
- Safely access nested properties with optional chaining.
const user: User | null = getUser(); // Without optional chaining (error if user is null) const name = user.profile.name; // Error // With optional chaining (safe) const name = user?.profile?.name; // undefined if user is null - Optional chaining returns undefined if any part is null or undefined.
const city = user?.address?.city; // undefined rather than throwing - Chain optional method calls with ?. before the parentheses.
const length = user?.getName?.(); // undefined if getName doesn't exist - Combine optional chaining with nullish coalescing for fallback values.
const displayName = user?.profile?.displayName ?? "Anonymous"; - Use optional chaining on array access and computed properties.
const firstTag = post?.tags?.[0]; // undefined if tags is absent const value = map?.["dynamic-key"]?.trim(); // safe dynamic key access
Nullish Coalescing
- Use ?? to provide default values only for null or undefined.
const name = user?.name ?? "Guest"; const count = value ?? 0; - Unlike ||, ?? does not replace empty strings or zero.
const count = 0; console.log(count || 10); // 10 — wrong, zero is falsy console.log(count ?? 10); // 0 — correct, zero is not null - Use ??= to assign a default only when the variable is null or undefined.
let config: Config | null = null; config ??= defaultConfig; // assigned only when null or undefined - Chain ?? with optional chaining for safe nested default values.
const role = user?.permissions?.role ?? "viewer"; - Use ?? in function parameters to handle missing arguments.
function paginate(page: number | null, size: number | null) { const p = page ?? 1; const s = size ?? 20; return { page: p, size: s }; }
Type Guards
- Check for null before accessing properties.
function printLength(value: string | null) { if (value !== null) { console.log(value.length); // Safe: value is string } } - Use typeof to guard primitive types.
if (typeof value === "string") { console.log(value.toUpperCase()); // Safe: narrowed to string } - Use instanceof to narrow class instances.
function handle(err: unknown) { if (err instanceof Error) { console.error(err.message); // Safe: narrowed to Error } } - Write a custom type guard function with a type predicate.
function isUser(value: unknown): value is User { return typeof value === "object" && value !== null && "name" in value; } if (isUser(data)) { console.log(data.name); // Safe: narrowed to User } - Use in operator to narrow discriminated union types.
type Cat = { meow(): void }; type Dog = { bark(): void }; function speak(animal: Cat | Dog) { if ("meow" in animal) animal.meow(); else animal.bark(); }
Optional Types
- Mark properties as optional with ? to allow undefined.
interface User { name: string; email?: string; // can be undefined phone: string | null; // can be null (must be explicit) } - Optional properties don't need to be included when constructing an object.
const user: User = { name: "Alice" // email is optional — safe to omit }; - Distinguish optional (?) from nullable (| null) for precise types.
interface Post { title: string; subtitle?: string; // missing or undefined deletedAt: Date | null; // always present, but can be null } - Mark function parameters optional to make them skippable.
function greet(name: string, title?: string): string { return title ? `Hello, ${title} ${name}` : `Hello, ${name}`; } greet("Alice"); // OK greet("Alice", "Dr."); // OK - Use Required
to remove optional modifiers when needed. interface Options { timeout?: number; retries?: number; } function runWithDefaults(opts: Required<Options>) { console.log(opts.timeout, opts.retries); // both guaranteed present }
Non-Null Assertion
- Use ! to tell TypeScript a value is not null or undefined.
const value = getValue(); const length = value!.length; // Assert value is not null - Prefer an explicit null check over the non-null assertion.
// Good: check first — safe at runtime if (value !== null) { console.log(value.length); } // Avoid: assertion without check — can throw at runtime console.log(value!.length); - Use ! on DOM queries when you know the element exists.
const input = document.getElementById("email")!; // Safe only if the element is guaranteed in the HTML - Avoid ! inside library code — callers may not share your assumptions.
// Better: surface the possibility of null in the return type function findUser(id: number): User | null { return db.users.find(u => u.id === id) ?? null; } - Use optional chaining as a safer alternative to non-null assertion.
// Assertion — crashes if null element!.classList.add("active"); // Optional chaining — silently skips if null element?.classList.add("active");
FAQ
Optional chaining (?.) short-circuits to undefined when it encounters null or undefined at any point in the chain, while && short-circuits to the falsy value itself (including 0 or empty string). Prefer ?. when you only care about null/undefined, and && when you need to guard against all falsy values.
Use ?? when your fallback should only trigger for null or undefined, not for other falsy values like 0, false, or empty string. For example, count ?? 0 keeps 0 as a valid value, whereas count || 0 would incorrectly replace it.
Use a type guard such as if (value !== null && value !== undefined) or if (value) before accessing properties — TypeScript will narrow the type inside the block. You can also write a custom type guard function with a value is Type return annotation for reusable narrowing logic.
An optional property (name?: string) can be omitted from the object entirely and has an implicit undefined, while an explicit union (name: string | null) requires the property to be present but allows null as a value. Choose optional for properties that may not exist, and string | null when the property must always be declared but can have no value.
This usually happens when the value is accessed outside the narrowing block, reassigned between the check and use, or when TypeScript can't track the control flow — for instance, after an async gap or inside a callback. Move the property access inside the guard block, or assign the narrowed value to a const before the async operation.