null & undefined
null and undefined are TypeScript’s two ways to express “no value,” and each is both a type and a single literal value. undefined typically means a value was never set — an uninitialized variable, a missing property, or a parameter that wasn’t passed. null is usually an intentional empty assigned by your code or returned by an API. How strictly the compiler tracks them depends entirely on the strictNullChecks option, which fundamentally changes the type system. This page covers both modes, the operators that make nullable code safe (?., ??, !), and when to choose one over the other.
Two types, two values
Each keyword is a type whose only inhabitant is the matching literal. On their own they are rarely useful; they shine inside unions with other types.
let a: undefined = undefined;
let b: null = null;
// Realistic usage: union with a "real" type
let middleName: string | undefined; // may be absent
let manager: Employee | null; // explicitly "no manager"
Declaring string | undefined says the value is a string or genuinely missing, while Employee | null models an explicit, deliberate emptiness. The distinction is a convention, but a valuable one for readers.
strictNullChecks changes everything
With strictNullChecks on (included in --strict), null and undefined are not members of every type — you must add them to a union explicitly. This is what catches the billion-dollar null-reference mistake at compile time.
// tsconfig.json → "strictNullChecks": true
let name: string = "Ada";
// ❌ Error: Type 'null' is not assignable to type 'string'
name = null;
function greet(user: string | null) {
// ❌ Error: 'user' is possibly 'null'
return user.toUpperCase();
}
Under strict mode the compiler forces you to acknowledge that user might be null before you dereference it. You narrow with a check (if (user)) or use the operators below. Turn this on for every new project.
Without strictNullChecks they vanish
When the flag is off, null and undefined are silently assignable to every type, and the compiler offers no protection. This is the legacy default and a frequent source of runtime crashes.
// With strictNullChecks: false
let name: string = null; // compiles, but dangerous
name.toUpperCase(); // 💥 runtime TypeError, no compile error
Because the absent values disappear from your types, the bugs surface only at runtime. Migrating to strictNullChecks: true surfaces them all at compile time instead.
Optional chaining: ?.
The optional chaining operator short-circuits a property access, call, or index when the value before it is null or undefined, yielding undefined instead of throwing.
type User = { profile?: { email?: string } };
function getEmail(user: User | null): string | undefined {
return user?.profile?.email; // stops at the first null/undefined
}
const len = user?.profile?.email?.length; // number | undefined
arr?.[0]; // optional index access
fn?.(); // optional call
If user is null, the whole expression evaluates to undefined rather than throwing. The result type automatically includes undefined, so the compiler keeps you honest downstream.
Nullish coalescing: ??
The ?? operator supplies a fallback only when the left side is null or undefined — unlike ||, it does not treat 0, "", or false as missing.
function withPort(port: number | undefined) {
const resolved = port ?? 8080; // only falls back on null/undefined
return resolved;
}
withPort(0); // 0 (kept — 0 is a valid port)
withPort(undefined); // 8080
// Contrast with ||, which would wrongly replace 0:
const wrong = 0 || 8080; // 8080 — bug for legitimate zero values
Use ?? whenever 0, empty string, or false are valid values you want to preserve. Reserve || for genuine boolean truthiness defaults.
Non-null assertion: !
The postfix ! tells the compiler “trust me, this is not null or undefined here,” removing those types without any runtime check. It is an escape hatch, not a safety feature.
function process(el: HTMLElement | null) {
el!.focus(); // asserts el is non-null — NO runtime guard is generated
}
If your assumption is wrong, you get the very runtime crash strictNullChecks was meant to prevent. Prefer a real check (if (el) el.focus()) and use ! only when you can prove non-nullness that the compiler cannot see.
null vs undefined
| Aspect | undefined | null |
|---|---|---|
| Typical meaning | Value never set / missing | Intentional, explicit empty |
| Source | Uninitialized vars, absent params/props, missing keys | Assigned by you or an API |
typeof result | "undefined" | "object" (historical quirk) |
Default for optional ? property | Yes (x?: T ⇒ T | undefined) | No |
| JSON support | Omitted / not valid JSON value | Valid JSON null |
| Equality | null == undefined is true; === is false | same |
Pick one convention for “absent” and stick to it. Many teams use
undefinedeverywhere internally and only acceptnullat API boundaries, since optional properties and missing arguments naturally produceundefined.
Best Practices
- Enable
strict(or at leaststrictNullChecks) intsconfig.jsonfor every project. - Model possible absence explicitly with
T | nullorT | undefined— never rely on implicit nullability. - Use
undefinedfor “not set” and reservenullfor deliberate, assigned emptiness. - Prefer
?.and??over manual&&/||chains;??preserves falsy-but-valid values like0and"". - Treat the non-null assertion
!as a last resort; favor narrowing with anifcheck. - Normalize
nullfrom external APIs toundefinedearly if your codebase standardizes on one. - Let narrowing (truthiness checks, equality) remove
null/undefinedso member access is provably safe.