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 stopsnullandundefinedfrom being allowed in every type. - Optional chaining ?.
- Reads a property or calls a function only if the thing before it isn't
nullorundefined, else givesundefined. - Nullish coalescing ??
a ?? bgivesa, unlessaisnullorundefined, then it givesb.- Falsy
- Treated as false in an
if:false,0,"",null,undefined,NaN. - Non-null assertion !
- A postfix
!that tells the checker a value is notnullorundefined, 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.
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).
function cityOfSafe(user: User): string {
return user.address?.city ?? "unknown";
}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.
const el = document.getElementById("app")!; // you promise: not null
let ready!: number; // you promise: assigned laterTry it yourself
- 1
Fix the crash with a check
In the first example rewrite
cityOfasif (user.address === null) return "unknown"; return user.address.city;. The error disappears: after theif, the checker knowsaddressis notnull. - 2
Pick the right fallback
Add
console.log(withOr(0) === withNullish(0));to the first example and predict the result before running. It printsfalse, because 10 is not 0. - 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 tryif (input) { input.focus(); }.
Code & diagrams
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 10The 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')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.
Break #2
Read a variable before assigning it
In the third example, console.log(total) reads total which has no value yet.
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.difais nullish the whole chain evaluates toundefinedand.cand.daren't read. It also works for calls (fn?.()) and indexes (list?.[0]). When?.is on a value that cannot be nullish, typescript-eslint'sno-unnecessary-conditionreports it. TypeScript itself only reports??when the left side is never nullish by its syntax, such as a literal ("a" ?? "b"isTS2869: Right operand of ?? is unreachable because the left operand is never nullish); for a variable typedstringit stays silent. - ▸
TypeScript narrows through
=== null,=== undefined,== null(which checks both) and truthiness. Narrowing has two deliberate gaps (Topic 16.1): a narrowedletvariable 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. - ▸
strictNullChecksis 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,nullandundefinedare in every type's domain andT | nullsimplifies toT.
Remember this
- 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. **
undefinedis 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:undefinedfor "not set yet",nullfor "deliberately empty". - 2
The trouble is what happens next. Reading a property of
nullorundefined(user.address.citywhenaddressisnull) crashes withTypeError: 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
TypeScript's answer is the flag **
strictNullChecks** (part ofstrict, so on in new projects). With it off,nullandundefinedcan be assigned to every type, and the checker is blind to the crash. With it on, they are separate types:stringno longer means "a string or null". A value that really may be missing says so in its type with a union: **string | nullornumber | undefined**. Whenever you use such a value the checker asks you to rule out the missing case first. - 4
Ruling it out takes one of four forms. A check:
if (user.address !== null) { ... }. **Optional chaining?.**:user.address?.cityreadscityonly ifaddressisn'tnullorundefined, and otherwise stops and givesundefinedfor the whole expression. **Nullish coalescing??**:a ?? "unknown"givesaunless it isnullorundefined, in which case it gives the fallback. And **!**, below. Optional chaining and??arrived in TypeScript 3.7 and are plain JavaScript today. - 5
Use
??, not||, for defaults.a || 10replaces *every falsy value*:0,""andfalsetoo, so a quantity of0becomes10.a ?? 10replaces onlynullandundefined, so0stays0(the example below runs both). The same care applies toif (value)checks on numbers and strings: the number0is a perfectly good value but is falsy (Topic 4.3 digs into this). - 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
Related: definite assignment. Declare
let total: number;with no value and read it before assigning, and the checker saysTS2454: 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, aninit()method),let ready!: number;or in a classname!: stringsilences it. As with!, the promise is yours to keep.
Explain it without notes
What problem does strictNullChecks solve?
What do ?. and ?? do, and why use ?? instead of || for defaults?
What does the non-null assertion ! do and when is it acceptable?
What is the difference between null and undefined?
What does "used before being assigned" mean and how can you fix it?
Practice
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 !.
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
- ↔
strictNullChecksadds handling code, but you write the check once and every caller after it is safe. The alternative is aTypeErrorin production. - ↔
Optional chaining can hide a real bug:
a?.b?.cquietly givesundefinedwhen 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
strictNullChecksaddresses it.I can write
T | nulland handle it with anif,?.and??.I can explain why
??is the right default operator and||is not.I can explain what
!andlet x!: Tpromise, and what happens if they are wrong.I can read
TS18047,TS18048andTS2454and fix them.