TypeScript course menu
null and undefined in TypeScript: strictNullChecks, Optional Chaining and Safe Defaults
Handle missing values safely in TypeScript: strictNullChecks, union types with null and undefined, optional properties versus explicit undefined, narrowing, optional chaining, nullish coalescing, the non-null assertion and its risks.
On this page
"Cannot read properties of undefined" is one of the most common JavaScript errors. TypeScript's strict null checks make it largely preventable: wherever a value could be missing, the compiler makes you handle that before using it.
strictNullChecks#
With "strict": true (which includes strictNullChecks), null and undefined are separate types that are not part of string, number or any other type. A variable that might be missing must say so with a union:
function findEmail(name: string): string | undefined {
const emails: Record<string, string> = { asha: "asha@example.com" };
return emails[name];
}
const email = findEmail("ravi");
console.log(email);undefinedUsing it without checking is an error:
function findEmail(name: string): string | undefined {
return name === "asha" ? "asha@example.com" : undefined;
}
const email = findEmail("ravi");
console.log(email.toUpperCase());error TS18048: 'email' is possibly 'undefined'.Without strict mode, TypeScript would accept this code, and it would crash at runtime. Always keep strict on.
Handling a possibly-missing value#
Narrow with a check#
function shout(text: string | null): string {
if (text === null) {
return "(nothing to say)";
}
return text.toUpperCase(); // text: string
}
console.log(shout("hello"), shout(null));HELLO (nothing to say)Optional chaining ?.#
Stop and return undefined if the value is missing:
type User = { name: string; address?: { city: string } };
const users: User[] = [{ name: "Asha", address: { city: "Pune" } }, { name: "Ravi" }];
for (const u of users) {
console.log(u.name, u.address?.city);
}Asha Pune
Ravi undefinedNullish coalescing ??#
Supply a default for null or undefined (but keep 0 and ""):
type Settings = { volume?: number; nickname?: string };
function describe(s: Settings): string {
const volume = s.volume ?? 50;
const nickname = s.nickname ?? "friend";
return `${nickname}: volume ${volume}`;
}
console.log(describe({}));
console.log(describe({ volume: 0, nickname: "" }));friend: volume 50
: volume 0Optional properties versus undefined#
These look similar but differ:
type A = { note?: string }; // the property may be absent
type B = { note: string | undefined }; // the property must be present, possibly undefined
const a: A = {};
const b: B = { note: undefined };
console.log("note" in a, "note" in b);false trueWith type B, writing const b: B = {} would be an error. Most of the time, optional properties (?) are what you want.
The non-null assertion !#
A ! after an expression tells TypeScript "trust me, this is not null or undefined":
const prices = new Map<string, number>([["tea", 20]]);
const teaPrice = prices.get("tea")!; // number, not number | undefined
console.log(teaPrice + 5);25It removes the compile-time check without adding any runtime check. If you are wrong, the program crashes just as plain JavaScript would:
const prices = new Map<string, number>([["tea", 20]]);
const coffeePrice = prices.get("coffee")!;
console.log(coffeePrice + 5);NaNHere there was no crash, but the bug silently produced NaN. Prefer a real check (if, ??, or throwing a clear error). Use ! only when you genuinely know more than the compiler, and keep such places rare.
A helper for required values#
When a missing value really is a bug, fail loudly with a clear message:
function required<T>(value: T | null | undefined, message: string): T {
if (value == null) throw new Error(message);
return value;
}
const config = { apiUrl: "https://api.example.com", timeoutMs: undefined as number | undefined };
const url = required(config.apiUrl, "apiUrl is not set");
console.log(url);
try {
required(config.timeoutMs, "timeoutMs is not set");
} catch (error) {
console.log((error as Error).message);
}https://api.example.com
timeoutMs is not set(<T> makes the function generic; see Generics.)
Practice#
Practice exercise
Write initials(user: { first: string; middle?: string; last?: string | null }): string that joins the first letters of the names that are present, in capitals, with full stops. Test with all three names, with only a first name, and with a null last name.
Show solution
function initials(user: { first: string; middle?: string; last?: string | null }): string {
return [user.first, user.middle, user.last]
.filter((part): part is string => part != null && part !== "")
.map((part) => part[0].toUpperCase() + ".")
.join("");
}
console.log(initials({ first: "asha", middle: "devi", last: "rao" }));
console.log(initials({ first: "meera" }));
console.log(initials({ first: "ravi", last: null }));A.D.R.
M.
R.The (part): part is string annotation tells TypeScript the filter removes null and undefined, so map receives plain strings. This is a type guard (Type Guards).
Key takeaway#
Finished this lesson?