Before you begin
Why this matters
A missing value read as 0, or a real 0 read as missing, gives a wrong answer and no error. Knowing how TypeScript writes “no value” shows you where that can happen.
What this helps you reason about
- Tell undefined and null from 0, false, "", and [].
- Predict when reading a property of a missing value throws an error.
- Spot a default or a special value that hides a missing value, and let the compiler find the rest.
A piano teacher collects a practice log from each pupil every week. Ada practised for 0 minutes. Ben handed in no log at all. Both are “nothing” in everyday speech, but they are different answers, and a program must keep them apart. TypeScript has two values for “nothing”, and a few operators that treat 0 as nothing too.
Already know this? The short version
- undefined (a let with no value, a missing property, an index past the end, Map.get of a missing key) and null (written on purpose; what JSON gives) both mean “no value”; 0, false, "", and [] are real values.
- Reading a property of undefined or null throws TypeError: Cannot read properties of undefined; the compiler does not check array indexes.
- || replaces every falsy value, so a real 0 or "" is lost; ?? replaces only undefined and null. A default such as ?? 0 turns “missing” into a real 0.
- A sentinel such as -1 works only when real data can never hold it; otherwise return number | undefined. With strictNullChecks (part of strict), the compiler refuses a possibly undefined value until the code checks it.
Words in this lesson (7)
- undefined
- A special JavaScript value that means “no value yet”. A let variable declared without a value holds undefined.
- union type
- A type made of several types joined with |, such as number | undefined: the value is a number, or it is undefined.
- null
- A JavaScript value that code writes on purpose to mean “no value”. It is a different value from undefined.
- nullish coalescing (??)
- The operator a ?? b. It gives b only when a is undefined or null, so it keeps "" and 0. a || b gives b whenever a is falsy, including "" and 0.
- TypeError
- The error JavaScript throws when an operation gets a value it cannot work with, such as reading a property of undefined.
- sentinel value
- A special value that a program agrees to read as “no value”, such as -1 for a weight that nobody entered. It works only when it can never be a real value.
- strictNullChecks
- A compiler setting, part of strict, that keeps undefined and null out of every type that does not list them, and refuses code that uses a value that may be missing.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / Four kinds
Are no value, zero, false, and empty the same thing?
Four different things are easy to confuse. A value can be missing: there is nothing there at all. It can be zero: a real number that happens to be 0. It can be false: a real answer to a yes-or-no question. Or it can be empty: a real list or text with nothing in it.
In a piano teacher’s practice log, Ada practised for 0 minutes: a real number. Ben handed in no log, so his minutes are missing. “Ada played scales” is false: a real answer. The teacher’s note for Ada is empty text, and her list of pieces for next week is an empty list.
In TypeScript, a missing value is usually undefined. The type number | undefined is a union type: the value is a number, or it is undefined. ben below has that type and no value yet, so it holds undefined. === shows that undefined is not 0.
const ada = 0; // practised, for 0 minutes
let ben: number | undefined; // no log
const scales = false;
const note = "";
const pieces: string[] = [];
console.log(ada === ben); // false
console.log(ben); // undefined Missing, zero, false, and empty are four different values; only the first means that nothing is there.
Coming from Python: TypeScript has two values for “nothing”, undefined and null, where Python has None.
Experiment
Tell a missing value from a real 0
A let declared without a value holds undefined. undefined is a value of its own, not a number, so === against 0 is false. Giving ben the value 0 makes it a real number, equal to Ada’s.
Why it matters Keep a missing value as undefined until a real value arrives. A missing value that starts at 0 cannot be told apart from real data.
Predict both lines of output. Then change let ben: number | undefined; to let ben: number | undefined = 0; and predict again.
const ada = 0; // practised, for 0 minutes
let ben: number | undefined; // no log
const scales = false; // did not play scales
const note = ""; // an empty note
const pieces: string[] = []; // no pieces set
console.log(ada === ben, ben);
console.log(scales, note.length, pieces.length);
What to look for
Before a line runs, the panel shows its name as not initialized yet. After line 2, ben shows undefined, while ada shows 0, scales false, note "", and pieces []: five different values. The run prints false undefined, then false 0 0. Step sometimes stops on the same line twice; that is normal for this debugger. After the edit, ben shows 0, and the first line becomes true 0: the missing log now looks like Ada’s real 0.
Expected output
false undefined
false 0 002 / No value
How is “no value” written?
TypeScript has two values that mean “no value”. undefined is what you get when nobody gave a value: a let declared without one, a property that an object does not have, an index past the end of an array, or Map.get with a key the map does not hold. null is a value that code writes on purpose to say “no value”, and JSON.parse gives null for null in the data.
Both are different from 0, false, "", and []. Destructuring and multiple bindings used ?? for a default: a ?? b gives b only when a is undefined or null, so it keeps a real 0 or "".
const logs = new Map<string, number>();
logs.set("Ada", 0);
console.log(logs.get("Ada")); // 0
console.log(logs.get("Ben")); // undefined
const data = JSON.parse('{"minutes": null}');
console.log(data.minutes); // null undefined means that nobody gave a value; null means that code said “no value” on purpose.
Precisely: undefined or null?
The language treats them almost alike. undefined === null is false, but undefined == null is true, and == treats each of them as equal to nothing else, so x == null tests for both. They differ in JSON: JSON.stringify leaves out a property whose value is undefined and writes null as null. A default parameter replaces undefined but not null. typeof null is "object", for historical reasons. Many code bases choose one of the two, usually undefined, and turn null from outside data into it.
03 / Crashes
What happens when code uses a value that is not there?
Code that treats a missing value as if it were there stops with an error. Reading a property of undefined throws a TypeError: the error JavaScript throws when an operation gets a value it cannot work with. The message names the property it tried to read: TypeError: Cannot read properties of undefined (reading 'pupil').
The compiler catches many of these before the program runs, as the last section shows. It does not check an array index. logs[2] asks for the element at position 2, counting from 0. Its type is Log, although the array below has no element there.
type Log = { pupil: string; minutes: number };
const logs: Log[] = [{ pupil: "Ada", minutes: 0 }];
const chosen = logs[2]; // undefined
console.log(chosen.pupil); // TypeError Reading a property of undefined or null throws a TypeError when that line runs.
Precisely: ?. and noUncheckedIndexedAccess
Optional chaining, chosen?.pupil, gives undefined instead of throwing when chosen is undefined or null. It passes the missing value on instead of handling it, so use it where “no value” is a normal answer. The compiler option noUncheckedIndexedAccess gives logs[2] the type Log | undefined, so the compiler checks indexes too.
Experiment
Read a property of a missing element
logs has elements at positions 0 and 1. logs[2] gives undefined, and the compiler does not check array indexes. Reading .pupil of undefined has nothing to read from, so JavaScript throws a TypeError.
Why it matters A TypeError that names undefined points to a value that was missing earlier. Look back to where that value came from: an index, a Map.get, or a property.
Predict what is printed. Then change logs[1] to logs[2] and predict what happens.
type Log = { pupil: string; minutes: number };
const logs: Log[] = [
{ pupil: "Ada", minutes: 0 },
{ pupil: "Cleo", minutes: 45 },
];
const chosen = logs[1];
console.log(chosen.pupil, chosen.minutes);
What to look for
After line 7, chosen shows {pupil: "Cleo", minutes: 45}, and the run prints Cleo 45. After the edit, chosen shows undefined, and the run stops at line 8 with TypeError: Cannot read properties of undefined (reading 'pupil'). Nothing is printed.
Expected output
Cleo 4504 / Sentinels
When can a special value mean “no value”?
Code can also choose an ordinary value of the right type to mean “no value”. Such a value is a sentinel value. A search that returns -1 for “not found” is safe, because a position is never -1.
A sentinel works only when real data can never hold it. Below, weekChange gives this week’s minutes minus last week’s, and -1 when there was no log last week. A pupil who practised one minute less than last week also gets -1, and the two answers cannot be told apart.
The fix is a result outside the number type: number | undefined, with undefined for “no log last week”.
// -1 means "no log last week".
function weekChange(last: number | undefined, now: number): number {
if (last === undefined) {
return -1;
}
return now - last;
}
console.log(weekChange(undefined, 20)); // -1: no log
console.log(weekChange(30, 29)); // -1: one minute less A sentinel value works only when real data can never hold it; otherwise return a separate “no value”.
05 / Mix-ups
How do a real 0 and “no value” get mixed up?
The quiet bugs come from mixing the four up, in either direction.
A real value taken for “no value”: a || b gives b whenever a is falsy. A falsy value is one that a condition treats as false: false, 0, "", NaN, undefined, and null. So ada || "no log" says “no log” for Ada, who practised for 0 minutes.
“No value” taken for a real value: ben ?? 0 turns Ben’s missing log into a real 0, and an average over all pupils then counts him as practising for 0 minutes. Use a default only where “missing” really means that value. Conditions and truthiness shows the same trap inside an if.
let ada: number | undefined = 0;
let ben: number | undefined = undefined;
console.log(ada || "no log"); // "no log": the 0 is lost
console.log(ada ?? "no log"); // 0
console.log(ben ?? 0); // 0: the missing log is lost || treats 0 and "" as missing, and ?? 0 treats missing as 0. Decide which one your data means.
Coming from Python: Python’s or has the same trap: 0 or 30 gives 30.
Coming from Java: Java has no truthiness; in TypeScript, 0, "", and NaN count as false in || and in conditions.
Experiment
Give a default to a real 0
|| gives its right side whenever the left side is falsy: false, 0, "", NaN, undefined, or null. ?? gives its right side only for undefined and null.
Why it matters Use ?? when 0 or "" is real data. Use || only when every falsy value should get the default.
Predict both lines. Then change both || to ?? and predict again.
let ada: number | undefined = 0;
let ben: number | undefined = undefined;
console.log("Ada:", ada || "no log");
console.log("Ben:", ben || "no log");
What to look for
ada shows 0 and ben shows undefined. The run prints Ada: no log, then Ben: no log. After the edit, the run prints Ada: 0, then Ben: no log.
Expected output
Ada: no log
Ben: no log06 / Types
How can a type make “no value” visible?
strictNullChecks is a compiler setting, part of the strict setting that the stepper uses. It keeps undefined and null out of every type that does not list them. A number can then never be undefined, and a value of type number | undefined must be checked before it is used as a number.
Map.get returns number | undefined for a Map<string, number>. Adding 5 to that result does not compile; the compiler reports 'ben' is possibly 'undefined'. Inside if (ben !== undefined), the compiler knows that ben is a number.
The check does not reach everything: an array index has the element type, as the crash section showed, and a value from JSON.parse has the type any, which the compiler does not check at all.
const logs = new Map<string, number>();
const ben = logs.get("Ben"); // number | undefined
// console.log(ben + 5); does not compile
if (ben !== undefined) {
console.log(ben + 5);
} With strictNullChecks, the type says whether a value can be missing, and the compiler makes you check it.
Precisely: the ! operator
ben! tells the compiler “this is not undefined or null” without any check. It removes the error, not the missing value: if ben is undefined, the program still fails when it runs.
07 / Try it
Can you explain a fresh example?
Decide for each value whether it is missing or real. The next lesson, Conditions and truthiness, asks which values an if treats as false.
const minutes = new Map<string, number>();
minutes.set("Ada", 0);
minutes.set("Dev", 20);
const ada = minutes.get("Ada");
const eli = minutes.get("Eli");
console.log(ada ?? "none", eli ?? "none", ada || "none");
What is printed?
Reveal the reasoning
0 none none. ada is Ada’s real 0, so ada ?? "none" keeps it. Eli has no entry, so minutes.get gives undefined, and ?? gives "none". ada || "none" replaces the real 0 too, because 0 is falsy.
Practice tickets
- ABSENT-101 · Instruments that nobody checked show as in tune → Free
- ABSENT-102 · Classes with an empty waiting list say they have none → Sign in · Pro
- ABSENT-103 · Quiet songs say that no take was kept → Sign in · Pro