Made by VeraStack Labs
Contents
The empty tiffin
You opened it expecting lunch.
It is lunch break. You pull your tiffin box out of the bag, open the lid, and reach in for a paratha. The box is empty. Somebody forgot to pack it.
Nothing in the box told you. It looked exactly like a full one from outside. You only found out when you reached in.
That is the most common crash in JavaScript. A value you expected to be there is not there. You reach into it anyway, and the program stops with "Cannot read properties of undefined".
JavaScript has two words for "nothing here". undefined means "nothing was ever put here": a missing property, a function that returned nothing, a find that found nothing. null means "someone chose to leave this empty". The difference is small, and in practice you treat them the same way: check before you reach in.
TypeScript's big idea here is simple. It can mark a tiffin box as "might be empty", and then refuse to let you reach in until you have looked. That setting is called strictNullChecks, and it is part of strict, which you turned on in lesson 1.
Remember: undefined is "never filled", null is "left empty on purpose". TypeScript can make you look before you reach in.
Reaching into nothing
JavaScript finds out when it crashes.
Here is the empty tiffin as code. The box has an owner and a lunch, and the lunch is null.
const tiffin = { owner: "Asha", lunch: null };console.log(tiffin.lunch.toUpperCase());TypeError: Cannot read properties of null (reading 'toUpperCase')A TypeError is JavaScript's name for "you used a value in a way its kind doesn't allow". Here, you asked null for a method.
The sneakier version comes from find. A find looks through a list and gives back the first match, or undefined if there is none.
const students = [ { roll: 1, name: "Asha" }, { roll: 2, name: "Ravi" },];function nameOf(roll) { return students.find((s) => s.roll === roll).name;}console.log(nameOf(1));console.log(nameOf(9));Asha
TypeError: Cannot read properties of undefined (reading 'name')Student 9 doesn't exist. find gave back undefined, and .name on it crashed. The code read fine. It worked in every test with a real student. It broke the first time somebody typed a wrong roll number.
Remember: in JavaScript, any value can be null or undefined, and nothing warns you before you reach in.
Mark the box maybe empty
With strictNullChecks, 'maybe' is part of the type.
In strict TypeScript, string means "always text". It can't be null. If a value might be empty, you say so with a union from lesson 5: string | null.
const tiffin = { owner: "Asha", lunch: null };console.log(tiffin.lunch.toUpperCase());Tinted lines are the ones that differ
The error says 'tiffin.lunch' is possibly 'null'. The checker saw the union and refused the method call. Now the fix is the narrowing you learned last lesson. Look first, then reach in.
type Tiffin = { owner: string; lunch: string | null };const tiffin: Tiffin = { owner: "Asha", lunch: null };if (tiffin.lunch === null) { console.log("The tiffin is empty");} else { console.log(tiffin.lunch.toUpperCase());}The tiffin is emptyIn the else branch, lunch is a plain string, so toUpperCase is allowed.
The find bug goes the same way. TypeScript knows that find may find nothing, so its answer has the type Student | undefined.
const students = [ { roll: 1, name: "Asha" }, { roll: 2, name: "Ravi" },];function nameOf(roll) { return students.find((s) => s.roll === roll).name;}console.log(nameOf(1));console.log(nameOf(9));Tinted lines are the ones that differ
JavaScript was happy. TypeScript stops you at .name. This is the bug that crashed in production, caught in the editor. The fix is a check.
type Student = { roll: number; name: string };const students: Student[] = [ { roll: 1, name: "Asha" }, { roll: 2, name: "Ravi" },];function nameOf(roll: number): string { const found = students.find((s) => s.roll === roll); if (found === undefined) { return "No such student"; } return found.name;}console.log(nameOf(1));console.log(nameOf(9));Asha
No such studentOnce found is checked against undefined, the early return means the lines after it only run when found is a real Student.
A function can't sneak a null or undefined out either. If it promises string, the checker refuses to let it return undefined.
function firstWord(text: string): string { const space = text.indexOf(" "); if (space === -1) { return undefined; } return text.slice(0, space);}TypeScript says
error TS2322: Type 'undefined' is not assignable to type 'string'.
Remember: a plain type never includes null or undefined. If a value might be missing, the type says so, and you check before you use it.
Three short tools
Optional chaining, ??, and the ! promise.
A check after every value would get tiring. JavaScript has two short forms, and the third tool is TypeScript's own.
1. Optional chaining: ?. Put a question mark before the dot. It means "if the thing on the left is null or undefined, stop and give undefined. Otherwise carry on".
type Address = { city: string };type Customer = { name: string; address?: Address };function cityOf(customer: Customer): string | undefined { return customer.address?.city;}console.log(cityOf({ name: "Asha", address: { city: "Pune" } }));console.log(cityOf({ name: "Ravi" }));Pune
undefinedRavi has no address, so customer.address?.city stops early and gives undefined. It never reaches into the empty box. The result type is string | undefined, so the checker still reminds you that the answer may be missing.
2. Nullish coalescing: ??. This picks a fallback, but only when the left side is null or undefined. Compare it with the || you may already use. The || replaces every falsy value, meaning any value that counts as "no" in an if: 0, an empty text, false, and also null and undefined.
function runsLabel(runs) { return "Runs: " + (runs || "did not bat");}console.log(runsLabel(82));console.log(runsLabel(0));console.log(runsLabel(undefined));Tinted lines are the ones that differ
Runs: 82
Runs: 0
Runs: did not batThat is the output of the TypeScript side. With ||, a batter who scored a duck (zero runs) was shown as "did not bat", because 0 counts as "no". With ??, only a truly missing score gets the fallback. A zero is a real answer.
3. The non-null assertion: !. An exclamation mark after a value tells the checker "I promise this isn't null or undefined, so stop asking". The checker believes you, and doesn't check. If you're wrong, it crashes at runtime exactly as JavaScript would.
Only use it when you can prove it from the lines just above. Here, we make sure the row exists, and then ask for it.
const seats = new Map<string, number>();function book(row: string): number { if (!seats.has(row)) { seats.set(row, 0); } // We just made sure the row exists, so get() cannot give undefined here. const next = seats.get(row)! + 1; seats.set(row, next); return next;}console.log(book("A"));console.log(book("A"));1
2We checked seats.has(row) and added the row when missing. So get(row) can't give undefined there. The ! is honest. But often there is a version with no promise at all.
const seats = new Map<string, number>();function book(row: string): number { const next = (seats.get(row) ?? 0) + 1; seats.set(row, next); return next;}console.log(book("A"));console.log(book("A"));1
2?? 0 says "if the row isn't there yet, start from zero". No promise, no crash, same answer. Prefer this one when it reads as well.
Remember: ?. stops at the first empty box, ?? replaces only null and undefined, and ! is a promise you must be able to prove.
Where 'maybe' hides
Four places the checker can't see the empty box.
strict has to be on. With strictNullChecks off, string quietly includes null and undefined, and every example in this lesson compiles without a word. The checker isn't broken. You switched off its best feature. Check that your tsconfig.json has "strict": true.
! is a promise, not a check. It adds no check at runtime. If you use it to silence an error you don't understand, you have only moved the crash from the editor to your user. Lesson 11 comes back to this.
?. can hide a real bug. If address should always be there, a long chain of ?. turns a loud crash into a quiet undefined that shows up three screens later. Use ?. where "missing" is a normal case, not to avoid thinking.
List items are trusted. names[10] on a list of three is undefined at runtime, but TypeScript still says string. A setting called noUncheckedIndexedAccess makes it string | undefined. It is stricter and takes some getting used to. For now, check the length, or use find and a loop.
Remember: keep strict on, treat ! as a promise, and use ?. where "missing" is expected.
Three empty boxes, handled
A missing city, a zero score, a missing contact.
Each problem starts with a JavaScript function that crashes or gives a wrong answer. Open it in the Playground and fix it with types.
Problem 1
A city that may be missing
Type cityLabel, so a customer with no address is allowed and gets the text "Unknown city". cityLabel({ name: "Ravi" }) should not crash.
Start here
function cityLabel(customer) { return customer.address.city;}console.log(cityLabel({ name: "Asha", address: { city: "Pune" } })); // "Pune"// cityLabel({ name: "Ravi" }) would crash: address is undefinedStuck?
Show a solution
type Address = { city?: string };type Customer = { name: string; address?: Address };function cityLabel(customer: Customer): string { return customer.address?.city ?? "Unknown city";}cityLabel({ name: "Asha", address: { city: "Pune" } }); // "Pune"cityLabel({ name: "Ravi" }); // "Unknown city"Both address and city are optional, so the type says they may be missing. The ?. stops at a missing address instead of crashing. The ?? supplies a fallback for any missing answer. The JavaScript version crashed for a customer with no address.
Problem 2
A duck is a score
Type scoreLine, and fix it so a player who scored 0 shows "Kohli: 0", and one who didn't bat shows "did not bat".
Start here
function scoreLine(player) { return player.name + ": " + (player.runs || "did not bat");}console.log(scoreLine({ name: "Kohli", runs: 0 })); // "Kohli: did not bat", which is wrongconsole.log(scoreLine({ name: "Bumrah" })); // "Bumrah: did not bat"Stuck?
Show a solution
type Player = { name: string; runs?: number };function scoreLine(player: Player): string { return player.name + ": " + (player.runs ?? "did not bat");}scoreLine({ name: "Kohli", runs: 0 }); // "Kohli: 0"scoreLine({ name: "Bumrah" }); // "Bumrah: did not bat"runs?: number allows a missing score. The ?? only replaces null and undefined, so a zero stays a zero. With ||, Kohli's duck was shown as 'did not bat', which is a quiet and unfair bug.
Problem 3
Pay a contact, if there is one
Type payTo, and handle a name that isn't in the list by returning a message, not by crashing.
Start here
const contacts = [ { name: "Asha", upi: "asha@upi" }, { name: "Ravi", upi: "ravi@upi" },];function payTo(name) { const contact = contacts.find((c) => c.name === name); return "Paying " + contact.upi;}console.log(payTo("Asha")); // "Paying asha@upi"// payTo("Meena") would crash: find gave back undefinedStuck?
Show a solution
type Contact = { name: string; upi: string };const contacts: Contact[] = [ { name: "Asha", upi: "asha@upi" }, { name: "Ravi", upi: "ravi@upi" },];function findContact(name: string): Contact | undefined { return contacts.find((c) => c.name === name);}function payTo(name: string): string { const contact = findContact(name); if (contact === undefined) { return "No contact named " + name; } return "Paying " + contact.upi;}payTo("Asha"); // "Paying asha@upi"payTo("Meena"); // "No contact named Meena"find gives back Contact | undefined. The check for undefined returns early, so the lines after it only run for a real contact, and contact.upi is allowed. The JavaScript version would crash for a name not in the list.
null in the interview
strictNullChecks, ?., ??, and the ! promise.
These come up in almost every TypeScript interview, often as a single question about "strict mode".
- "What does
strictNullChecksdo?" It stopsnullandundefinedfrom being allowed in every type. A value that might be missing has to say so, as instring | null, and you must check it before use. - "What is the difference between
nullandundefined?"undefinedmeans a value was never set.nullmeans it was set to empty on purpose. TypeScript tracks them as two separate types. - "What is the difference between
?.and???"?.stops a chain and givesundefinedif something on the way is missing.??gives a fallback when the left side isnullorundefined. - "Why
??and not||?"||replaces every falsy value, including0, an empty text andfalse.??replaces onlynullandundefined. - "What is the
!operator?" The non-null assertion. It tells the checker a value isn'tnullorundefined. It does no checking, so use it only when you can prove the value is there. - "How do you check for both
nullandundefined?"value == null(with two equals signs) matches both. Many teams otherwise write the two checks out. - "What does
findreturn in TypeScript?" The item type orundefined, so you must check before using it. - Say it simply. "With strict null checks, 'might be empty' is part of the type. I check, or use
?.and??, before I reach in" is a good answer.
Updated 2026-10-17
Made by VeraStack Labs