Technology · TypeScript
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.
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");
Tip: Enable strictNullChecks in tsconfig.json to catch null safety issues at compile time.
Warning: Non-null assertions (!) bypass type checking — use them only when you're certain the value is not null.