Command Palette

Search for a command to run...

Hectal
PHASE 1Beginner ~21 min· topic 7 of 7

Topic 1.7

null, undefined and strictNullChecks

In one line

With strictNullChecks (part of strict), null and undefined are not allowed everywhere. A value that might be missing says so in its type (T | null), and the checker makes you handle it before use.

Think of it like this

A parcel locker with 20 doors. Some doors are empty. If you walk to door 7 and reach in without looking, you may grab nothing and think you are holding something. Strict null checking is a rule that every locker door has a window. If a door might be empty, the label says "maybe empty", and you must look before you reach in.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

undefined
The value of something never set: a missing property, an uninitialised variable, an out-of-range item.
null
A value placed on purpose to mean "nothing here".
strictNullChecks
A compiler flag (part of strict) that stops null and undefined from being allowed in every type.
Optional chaining ?.
Reads a property or calls a function only if the thing before it isn't null or undefined, else gives undefined.
Nullish coalescing ??
a ?? b gives a, unless a is null or undefined, then it gives b.
Falsy
Treated as false in an if: false, 0, "", null, undefined, NaN.
Non-null assertion !
A postfix ! that tells the checker a value is not null or undefined, with no proof.
Definite assignment
The rule that a variable must have a value before it is read. ! after the name says you'll handle it.
TypeError
The runtime error JavaScript throws, for example when you read a property of null.

Step by step

01The crash JavaScript allows

user.address.city is fine until a user has no address. Then address is null and the line throws TypeError: Cannot read properties of null.

Nothing warned you. You find out when a real user's page breaks.

02Say it in the type

With strictNullChecks on, a missing-able value must say so: address: { city: string } | null. The type means "an object with a city, or null".

Reading .city straight away is now TS18047: 'user.address' is possibly 'null'. The checker found the crash before any user did.

maybe-null.tswhole filets
type User = { name: string; address: { city: string } | null };

function cityOf(user: User): string {
  return user.address.city;
  // error TS18047: 'user.address' is possibly 'null'.
}

03Handle it: check, ?. and ??

user.address?.city stops at null and gives undefined; ?? "unknown" then supplies a default text. The whole expression is a string.

An if check works too, and inside it the checker knows the value is not null. These are the same tools you will use for T | undefined from optional properties (Topic 1.5) and array indexes (Topic 1.4).

handled.tswhole filets
function cityOfSafe(user: User): string {
  return user.address?.city ?? "unknown";
}
Handle it: check, ?. and ??diagram
Rendering diagram…

04|| versus ??

|| replaces every falsy value. A quantity of 0 becomes 10, which is wrong.

?? replaces only null and undefined, so 0 survives. For defaults, ?? is almost always what you mean.

05The ! and the definite assignment promise

! after a value silences the checker but does not remove the risk. getElementById returns HTMLElement | null, because the element might not exist.

let total: number; read before it is assigned is TS2454. let ready!: number; removes the error, and now the program reads undefined, with no warning, if you were wrong.

promises.tswhole filets
const el = document.getElementById("app")!;   // you promise: not null
let ready!: number;                           // you promise: assigned later

Try it yourself

  1. 1

    Fix the crash with a check

    In the first example rewrite cityOf as if (user.address === null) return "unknown"; return user.address.city;. The error disappears: after the if, the checker knows address is not null.

  2. 2

    Pick the right fallback

    Add console.log(withOr(0) === withNullish(0)); to the first example and predict the result before running. It prints false, because 10 is not 0.

  3. 3

    Guard the element

    In the second example replace the last line with input?.focus();. Both the error and the crash are gone: if the element is missing, nothing happens. Then try if (input) { input.focus(); }.

Code & diagrams

Handling a value that may be null: ?. and ?? TypeScript 3.7+

cityOf is the bug: it is reported but never called here. The last line shows || turning a legitimate 0 into 10, while ?? keeps the 0 and only replaces undefined.

Console

(nothing was printed)

Expected console output

unknown Pune
10 0 10
getElementById may not find anything

The element nope does not exist, so the runtime really does crash, as the Console shows. The checker saw it coming: the type includes null because the element might not be on the page.

Console

(nothing was printed)

Expected console output

Uncaught TypeError: Cannot read properties of null (reading 'focus')
Promises the checker can't verify: definite assignment and !

Only the first line is an error. The ready line and the ! line are accepted, because you promised, and the Console shows both promises broken: undefined twice, then the same crash as the previous example, now with no warning.

Console

(nothing was printed)

Expected console output

undefined
undefined
Uncaught TypeError: Cannot read properties of null (reading 'focus')

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Read through a possibly-null value

In the first example, cityOf reads .city of user.address, which can be null.

terminal
$ Types panel
── what you'll see ──
index.ts(4,10): error TS18047: 'user.address' is possibly 'null'.

Break #2

Read a variable before assigning it

In the third example, console.log(total) reads total which has no value yet.

terminal
$ Types panel
── what you'll see ──
index.ts(2,13): error TS2454: Variable 'total' is used before being assigned.

Myth vs fact

Myth

null and undefined are the same.

Fact

They are different values and different types. Equality == treats them alike (null == undefined), but === does not. Pick one convention for "empty" in your code.

Myth

! fixes null errors.

Fact

It only hides them. If the value is null at runtime, you get the same crash you had in plain JavaScript, and the checker won't have warned you.

Myth

a || b and a ?? b are interchangeable defaults.

Fact

|| also replaces 0, "", false and NaN. For numbers and text that are allowed to be empty or zero, use ??.

Myth

strictNullChecks slows the program down.

Fact

It is a compile-time check only. There is no runtime cost.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Optional chaining short-circuits: in a?.b.c.d if a is nullish the whole chain evaluates to undefined and .c and .d aren't read. It also works for calls (fn?.()) and indexes (list?.[0]). When ?. is on a value that cannot be nullish, typescript-eslint's no-unnecessary-condition reports it. TypeScript itself only reports ?? when the left side is never nullish by its syntax, such as a literal ("a" ?? "b" is TS2869: Right operand of ?? is unreachable because the left operand is never nullish); for a variable typed string it stays silent.

  • ▸

    TypeScript narrows through === null, === undefined, == null (which checks both) and truthiness. Narrowing has two deliberate gaps (Topic 16.1): a narrowed let variable that is assigned again can lose its narrowing inside a callback, because the callback may run later; and a narrowed property stays narrowed across a function call even though the call might have changed it.

  • ▸

    strictNullChecks is the single most valuable strict flag. Turning it on late in a project can mean thousands of errors, so migrations enable it per file or per folder; a plan is in Topic 15.4. With it off, null and undefined are in every type's domain and T | null simplifies to T.

Remember this

  1. 1

    The problem this solves: A value that is sometimes missing crashes the program the moment you use it, with the error "cannot read properties of undefined". Strict null checking makes you handle the missing case first. **undefined is the value JavaScript gives to anything that was never set: a variable with no value, a missing property, an item past the end of a list, a function that returns nothing. null** is a value *you* put somewhere on purpose to mean "there is nothing here". They are two different values with two different types, and the difference is mostly convention: undefined for "not set yet", null for "deliberately empty".

  2. 2

    The trouble is what happens next. Reading a property of null or undefined (user.address.city when address is null) crashes with TypeError: Cannot read properties of null. This is the most common runtime error in JavaScript. Its inventor, Tony Hoare, who added the null reference to a language in 1965, later called it his billion-dollar mistake because of the bugs and crashes it caused across the industry.

  3. 3

    TypeScript's answer is the flag **strictNullChecks** (part of strict, so on in new projects). With it off, null and undefined can be assigned to every type, and the checker is blind to the crash. With it on, they are separate types: string no longer means "a string or null". A value that really may be missing says so in its type with a union: **string | null or number | undefined**. Whenever you use such a value the checker asks you to rule out the missing case first.

  4. 4

    Ruling it out takes one of four forms. A check: if (user.address !== null) { ... }. **Optional chaining ?.**: user.address?.city reads city only if address isn't null or undefined, and otherwise stops and gives undefined for the whole expression. **Nullish coalescing ??**: a ?? "unknown" gives a unless it is null or undefined, in which case it gives the fallback. And **!**, below. Optional chaining and ?? arrived in TypeScript 3.7 and are plain JavaScript today.

  5. 5

    Use ??, not ||, for defaults. a || 10 replaces *every falsy value*: 0, "" and false too, so a quantity of 0 becomes 10. a ?? 10 replaces only null and undefined, so 0 stays 0 (the example below runs both). The same care applies to if (value) checks on numbers and strings: the number 0 is a perfectly good value but is falsy (Topic 4.3 digs into this).

  6. 6

    The **non-null assertion !** (document.getElementById("app")!) tells the checker "I know this isn't null". It removes the error but not the risk: if you are wrong the program crashes at runtime, exactly like in plain JavaScript (see the last example). It is a promise you make without proof; prefer a real check or ?., and use ! only where the program's structure guarantees the value (Topic 5.6 covers assertions generally).

  7. 7

    Related: definite assignment. Declare let total: number; with no value and read it before assigning, and the checker says TS2454: Variable 'total' is used before being assigned. If you know a value will be set in a way the checker can't see (a framework fills it in, an init() method), let ready!: number; or in a class name!: string silences it. As with !, the promise is yours to keep.

Explain it without notes

01

What problem does strictNullChecks solve?

02

What do ?. and ?? do, and why use ?? instead of || for defaults?

03

What does the non-null assertion ! do and when is it acceptable?

04

What is the difference between null and undefined?

05

What does "used before being assigned" mean and how can you fix it?

Practice

01

Write function initials(name: string | null): string that returns the first letter of the name in upper case, or ? when the name is null or an empty string. Use no !.

02

A shop uses const shown = discount || 100; and a customer with a 0 discount sees 100. Explain the bug and fix it, given discount: number | undefined.

Trade-offs

  • ↔

    strictNullChecks adds handling code, but you write the check once and every caller after it is safe. The alternative is a TypeError in production.

  • ↔

    Optional chaining can hide a real bug: a?.b?.c quietly gives undefined when you expected data. Use it where missing data is a normal state, and fail loudly (throw, or return an error value, Topic 13.5) where it is not.

Done when you can

  • I can explain the billion-dollar mistake and how strictNullChecks addresses it.

  • I can write T | null and handle it with an if, ?. and ??.

  • I can explain why ?? is the right default operator and || is not.

  • I can explain what ! and let x!: T promise, and what happens if they are wrong.

  • I can read TS18047, TS18048 and TS2454 and fix them.