Chapter 18: TypeScript Null & Undefined
Null and undefined in TypeScript very honestly and thoroughly — like we’re sitting together debugging real code and trying to understand why this topic confuses so many people (even experienced developers).
1. First — the core truth most tutorials skip
JavaScript has two different ways to say “no value here”:
| Value | Meaning | Who/What usually creates it | Typical origin |
|---|---|---|---|
| undefined | “This thing has not been given a value yet” | JavaScript engine (most of the time) | Declared but not assigned, missing property, function without return |
| null | “I intentionally set this to no value” | You (the programmer) or some API | obj.prop = null, JSON responses, database nulls, explicit reset |
Very important sentence to remember forever:
In real-world TypeScript code written in 2025–2026 most bugs and most type errors involving null/undefined come from treating them the same or forgetting one of them exists.
2. How TypeScript sees them by default
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
let a: undefined; // only undefined is allowed let b: null; // only null is allowed let c: string | undefined; // string or undefined let d: string | null; // string or null let e: string | null | undefined; // all three (very common) |
By default → null and undefined are two separate types.
3. strictNullChecks = true (the setting that changes everything)
This is the single most important TypeScript setting for real applications.
|
0 1 2 3 4 5 6 7 8 9 10 11 |
// tsconfig.json { "compilerOptions": { "strictNullChecks": true // ← almost every serious project has this on } } |
When strictNullChecks: true:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
let name: string = "Sara"; // name = null; // Error! // name = undefined; // Error! let maybeName: string | null = null; // ok maybeName = "John"; // ok maybeName = undefined; // Error! (unless you also allow undefined) |
Realistic modern type you see everywhere:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
type User = { id: number; name: string; email: string | null; // API can return null phone?: string; // optional → type becomes string | undefined lastLogin: Date | null; // can be null if never logged in }; |
4. The most common real patterns in 2025–2026 code
Pattern 1: Optional chaining is your best friend
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const user: User | null = await fetchUser(); // Old dangerous way const emailLength = user.email.length; // boom if user = null // Modern safe ways const emailLength1 = user?.email?.length; // number | undefined const emailLength2 = user?.email?.length ?? 0; // number (fallback) const emailLength3 = user!.email.length; // non-null assertion — risky |
Pattern 2: Nullish coalescing ?? vs ||
|
0 1 2 3 4 5 6 7 |
const displayName = user.name || "Guest"; // "" → Guest (sometimes wrong) const displayNameSafe = user.name ?? "Guest"; // only null/undefined → Guest (usually correct) |
Pattern 3: Definite assignment assertion ! (when you’re sure)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class UserService { private currentUser!: User; // I promise I'll set it before using login(user: User) { this.currentUser = user; } getName() { return this.currentUser.name; // no error because of ! } } |
Pattern 4: Type narrowing (if / typeof / ===)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function printEmail(user: User | null) { if (user === null) { console.log("No user"); return; } // TypeScript now knows user: User console.log(user.email.toLowerCase()); } |
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function process(value: string | null | undefined) { if (value == null) { // checks both null and undefined console.log("missing"); return; } // value is now string console.log(value.toUpperCase()); } |
5. Quick comparison table (keep this in mind)
| Check / Operator | What it checks | Use when you want… | Safe with strictNullChecks? |
|---|---|---|---|
| value == null | null or undefined | Most common “missing value” check | Yes |
| value === null | only null | Distinguish null from undefined | Yes |
| value === undefined | only undefined | Check optional properties / missing args | Yes |
| value ? … : … | truthy / falsy | “” → false, 0 → false (sometimes unwanted) | Yes |
| value ?? … | nullish (null or undefined) | Keep “” and 0 | Yes — very popular |
| value?.prop | safe navigation | Avoid crashes on null/undefined | Yes |
| value! | non-null assertion | You are 100% sure it exists | Yes — but use sparingly |
6. Very common real bugs people still make (2026 edition)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// ❌ Classic mistake const data = await response.json(); // any const name = data.name; // no error — but runtime boom possible // Better type ApiResponse = { name: string } | null; const data = await response.json() as ApiResponse | null; if (!data) throw new Error("No data"); console.log(data.name); // safe now |
|
0 1 2 3 4 5 6 7 8 9 10 |
// ❌ Dangerous in forms / state const initialValues = { email: user?.email }; // email: string | undefined // Better: provide fallback const initialValues = { email: user?.email ?? "" }; |
7. Summary — modern mindset 2025–2026
- Use strictNullChecks: true (almost always)
- Prefer ?? over || for default values
- Love optional chaining ?.
- Use == null when you want to catch both null & undefined
- Narrow types with if, typeof, === null, === undefined
- Use ! only when you’re really sure (and it’s a small scope)
- When dealing with APIs → prefer null for intentional absence, undefined for “not present in object”
- In forms / state → lean towards “” / 0 / false instead of null/undefined when possible
Want to practice together?
Pick one of these and tell me — we can write & fix examples live:
- Safe API response handling
- Form default values with null/undefined
- Deep nested object access without crashes
- Distinguishing null vs undefined in real code
- Writing a generic non-null assertion helper
Which one sounds most useful for you right now? 😄
