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.
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.
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:
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:
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:
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:
let email: string | null | undefined; // could be eitherStrictNullChecks: 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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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 || 10 | 10 | 0 |
| 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:
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 ??
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 reasonThe 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:
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:
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
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:
const delay: number = config.timeout ?? 5000;For more on optional properties, see optional properties in TypeScript.
Rune AI
Key Insights
- Enable
strictNullCheckssonullandundefinedcannot 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 isnullorundefined. - Avoid the non-null assertion (
!) unless you have proven the value exists through other means.
Frequently Asked Questions
What is the difference between `null` and `undefined` in TypeScript?
Why does TypeScript complain when I assign `null` to a `string`?
Should I use optional chaining everywhere?
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.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.