Null and Undefined in TypeScript

Null means no value, undefined means not assigned yet. Learn how TypeScript's strictNullChecks prevents the most common runtime error in JavaScript, and how to safely handle missing values.

7 min read

Null and undefined both mean "no value" in TypeScript, but they signal different things. null is an intentional empty value that gets set on purpose. undefined is what JavaScript gives back when a variable has not been assigned or a property does not exist.

The compiler treats them as distinct types and, with the right settings, blocks either one from standing in for a real value.

typescripttypescript
let name: string = null;

The assignment looks harmless, but it gets rejected immediately because the annotation promised text and nothing else. The compiler stops the file from compiling and reports the exact mismatch before any code runs:

texttext
Type 'null' is not assignable to type 'string'.

This error is the single most valuable protection the language provides. In plain JavaScript, passing a missing value where text is expected causes a runtime crash. Here, the mistake gets caught before the code ever runs.

The Difference Between Null and Undefined

A variable becomes undefined when it is declared but never assigned, when a function returns nothing, when an object property does not exist, or when an optional parameter is omitted. A variable becomes empty on purpose only when code explicitly sets it that way, such as a failed element lookup in the browser.

Each gets its own type, and in strict mode they are not interchangeable with each other:

typescripttypescript
let a: null = null;
let b: undefined = undefined;
 
a = undefined; // Error: Type 'undefined' is not assignable to type 'null'

The reverse assignment fails for the same reason, just with the two types swapped, since neither one is considered a substitute for the other under strict mode:

typescripttypescript
b = null; // Error: Type 'null' is not assignable to type 'undefined'

In practice, you rarely annotate a variable this way directly. Instead, a union type says a value might be missing:

typescripttypescript
let email: string | null | undefined; // could be either

StrictNullChecks: The Essential Compiler Option

The behavior above depends on a flag called strictNullChecks. This option is part of strict mode in the compiler configuration, which is the recommended default for every new project.

Without it, a missing value is silently assignable to every type, so a mistake like this compiles cleanly and only fails once the code actually runs:

typescripttypescript
let name: string = null;
console.log(name.toUpperCase());

Nothing stops this from compiling without the flag, so the mistake only surfaces once the code actually executes and tries to call a method on a value that turns out to be empty:

texttext
TypeError: Cannot read properties of null (reading 'toUpperCase')

With the flag enabled, the same assignment is blocked at the source, before the file even compiles, which is the entire point of turning it on:

typescripttypescript
let name: string = null;

This time the compiler catches the same mistake immediately, without ever generating JavaScript that could crash later, because the flag turns the assignment itself into a hard stop:

texttext
Type 'null' is not assignable to type 'string'.

This single option prevents the most common category of JavaScript runtime errors. For more on configuring the compiler, see strict mode in TypeScript.

How to Narrow a Possibly-Missing Value

Once a variable has a union type like string | null, it cannot be used as plain text until the code proves the value is not missing. This is called narrowing.

Simple If Check

The most straightforward way to narrow is a plain if statement that checks for the missing case before using the value:

typescripttypescript
function greet(name: string | null): string {
  if (name === null) {
    return "Hello, stranger!";
  }
  return `Hello, ${name}!`; // name is string here
}

Calling the function with each kind of input shows both branches in action, one where the fallback greeting runs and one where the real name is used:

typescripttypescript
console.log(greet("Alice")); // "Hello, Alice!"
console.log(greet(null));    // "Hello, stranger!"

The compiler tracks the check across both branches. Inside the if block, the parameter is treated as missing. After the block, it narrows to a plain string, and both paths must be handled before the value can be used.

The flowchart below shows how this narrowing plays out:

Narrowing a string or missing value

The check splits the union into two branches, and each one gets the correct narrower type, so string methods are only reachable after the compiler confirms a value actually exists.

Optional Chaining

Optional chaining (?.) safely reads a property on a value that might be missing. If the left side is empty, the expression short-circuits and returns undefined instead of throwing:

typescripttypescript
type User = { name: string; address?: { city: string } };
 
function getCity(user: User | null): string {
  return user?.address?.city ?? "Unknown";
}

Calling this with a full address, a missing address, and a missing user all resolve safely instead of crashing, because each optional-chain step bails out the moment it hits an empty value:

typescripttypescript
const alice: User = { name: "Alice", address: { city: "Paris" } };
const bob: User = { name: "Bob" };
 
console.log(getCity(alice)); // "Paris"
console.log(getCity(bob));   // "Unknown"

Each step checks the left-hand side, and if it is empty, the whole expression short-circuits with no error thrown. Without it, reading a nested property directly would crash whenever the address is missing. The same operator also works on method calls and array access.

Nullish Coalescing

The nullish coalescing operator (??) provides a fallback value only when the left-hand side is empty. It is stricter than ||, which falls back on any falsy value, not just a missing one:

typescripttypescript
function getDisplayName(name: string | null | undefined): string {
  return name ?? "Anonymous";
}
 
console.log(getDisplayName(""));  // "" (empty string is not nullish)

An empty string is falsy but not nullish, so nullish coalescing correctly leaves it alone here, while the logical OR operator would have replaced it with the fallback. The table below compares the two operators directly:

Expression|| result?? result
"" || "Fallback""Fallback"""
0 || 10100
missing ?? "Fallback""Fallback""Fallback"

Reach for nullish coalescing when the only values that should trigger a fallback are the two empty ones. Reach for the logical OR operator when every falsy value should trigger it, including an empty string, zero, and false.

The Non-Null Assertion

The non-null assertion operator (!) tells the compiler to treat a value as present without any runtime check:

typescripttypescript
function getLength(value: string | null): number {
  return value!.length; // "I know this value is not missing"
}

This compiles, but it is dangerous: if the value is actually missing at runtime, the code crashes with a TypeError, because the assertion suppresses the type error without adding any real safety.

Only reach for the assertion when the code has proven the value exists through logic the compiler cannot track on its own, such as a framework lifecycle guarantee. For every other case, prefer an if check, optional chaining, or nullish coalescing instead.

Common Mistakes

Using || When You Mean ??

typescripttypescript
function getCount(value: number | null): number {
  return value || 0; // wrong: 0 is a valid value that gets replaced
}
 
console.log(getCount(0)); // 0, but for the wrong reason

The function above returns 0 whether the input was a real zero or a missing value, which hides a real bug: a valid zero and an absent value produce the exact same output. Swap the logical OR operator for nullish coalescing so only the two empty cases trigger the fallback:

typescripttypescript
function getCount(value: number | null): number {
  return value ?? 0; // right: only a missing value triggers the fallback
}

Overusing the Non-Null Assertion

Reaching for the assertion on every line that touches a nullable value defeats the purpose of the check in the first place:

typescripttypescript
function process(user: User | null) {
  console.log(user!.name!);
  console.log(user!.email!);
}

Every non-null assertion is a potential runtime crash. Stacking several of them in the same function is usually a sign that the value should not be optional in the first place, so refactor the function signature or add a proper check instead.

Forgetting That Optional Properties Are Undefined

typescripttypescript
type Config = {
  url: string;
  timeout?: number;
};
 
function connect(config: Config) {
  const delay: number = config.timeout; // Error: 'number | undefined' is not assignable to 'number'
}

An optional property adds undefined to its type automatically, even though the question mark makes that easy to forget. Add a fallback to handle the missing case explicitly:

typescripttypescript
const delay: number = config.timeout ?? 5000;

For more on optional properties, see optional properties in TypeScript.

Rune AI

Rune AI

Key Insights

  • Enable strictNullChecks so null and undefined cannot sneak into your types.
  • Narrow nullable types with if (value !== null) checks before using the value.
  • Use optional chaining (?.) for safe property access on possibly-null values.
  • Use nullish coalescing (??) to provide fallback values only when the value is null or undefined.
  • Avoid the non-null assertion (!) unless you have proven the value exists through other means.
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between `null` and `undefined` in TypeScript?

`null` is an intentional absence of a value. It is something you explicitly set. `undefined` means a variable was declared but not assigned a value, or a function returned nothing. TypeScript treats them as separate types, so `null` is not assignable to `string` unless you include it in a union like `string | null`.

Why does TypeScript complain when I assign `null` to a `string`?

With `strictNullChecks` enabled, `null` and `undefined` are not assignable to other types. This prevents the most common runtime error in JavaScript. If a value can be null, declare the type as `string | null` explicitly.

Should I use optional chaining everywhere?

Use optional chaining (`?.`) when a value might genuinely be `null` or `undefined` and you want to safely access a property. Do not use it when you are sure the value exists, because it hides bugs where a value is unexpectedly missing.

Conclusion

Null and undefined are the source of countless JavaScript runtime crashes. TypeScript's strictNullChecks turns those crashes into compile-time errors by refusing to let you use a possibly-missing value without checking it first. Narrow with simple if checks, use optional chaining for safe property access, reach for the nullish coalescing operator when you need a fallback, and avoid the non-null assertion unless you are absolutely certain a value exists.