Skip to content
Laviyx Learn

Try

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.

Intermediate7 min readUpdated
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:

TypeScript
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);
Output
undefined

Using it without checking is an error:

TypeScript
function findEmail(name: string): string | undefined {
  return name === "asha" ? "asha@example.com" : undefined;
}
const email = findEmail("ravi");
console.log(email.toUpperCase());
Compiler error
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#

TypeScript
function shout(text: string | null): string {
  if (text === null) {
    return "(nothing to say)";
  }
  return text.toUpperCase();     // text: string
}
console.log(shout("hello"), shout(null));
Output
HELLO (nothing to say)

Optional chaining ?.#

Stop and return undefined if the value is missing:

TypeScript
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);
}
Output
Asha Pune
Ravi undefined

Nullish coalescing ??#

Supply a default for null or undefined (but keep 0 and ""):

TypeScript
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: "" }));
Output
friend: volume 50
: volume 0

Optional properties versus undefined#

These look similar but differ:

TypeScript
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);
Output
false true

With 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":

TypeScript
const prices = new Map<string, number>([["tea", 20]]);

const teaPrice = prices.get("tea")!;   // number, not number | undefined
console.log(teaPrice + 5);
Output
25

It removes the compile-time check without adding any runtime check. If you are wrong, the program crashes just as plain JavaScript would:

TypeScript
const prices = new Map<string, number>([["tea", 20]]);
const coffeePrice = prices.get("coffee")!;
console.log(coffeePrice + 5);
Output
NaN

Here 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:

TypeScript
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);
}
Output
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
TypeScript
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 }));
Output
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?