null, undefined & strictNullChecks
See how TypeScript makes you handle values that may be missing: strictNullChecks, ?. and ??, the ! assertion, and optional parameters, in code you can break.
- 01Read a missing-value errorExplain
string | nulland'sort' is possibly 'null', and what strictNullChecks changes. - 02Pick the right toolHandle a value that may be missing with a check,
?.,??, or!, and know what each does when the value really is missing. - 03Tell optional from undefinedKnow when a parameter or property can be left out, and when it must be written even if it's
undefined.
A value that might not be there
A search page reads its web address to see what the shopper asked for. The address ?q=tea asks for tea, with no sort order. Here is plain JavaScript asking for the sort order anyway:
const params = new URLSearchParams("?q=tea");
const sort = params.get("sort");
console.log(sort.toUpperCase());Line 1 reads the part of the address after ? with URLSearchParams (the URL objects lesson covers it). Line 2 asks for sort. The address has no sort, so get gives back null, JavaScript's value for "nothing here". Line 3 calls .toUpperCase() on null, and the code stops with TypeError: Cannot read properties of null. Nothing warned you before it ran.
Here is the same code in TypeScript:
const params = new URLSearchParams("?q=tea");const sort = params.get("sort");// ^?console.log(sort.toUpperCase());The query on line 3 shows the type TypeScript gives sort: string | null. The | means "or", so sort is a string or null. TypeScript marks line 4, on purpose here, because it's the bug:
'sort' is possibly 'null'means "this value might benull, and you're using it as if it can't be".ts(18047)is the error's number. A value that might beundefinedgets a sister error,ts(18048), which you'll meet below.
params.get gives back text or null, so sort may hold either card. TypeScript marks line 4, because null has no toUpperCase.A friend says they'll leave your car keys at a hotel's front desk. When you ask, the desk may hand you the keys, or say nothing was left for you. A careful friend reminds you of the second answer, so you look in your hand before you walk out to the car park.
- In real life: You ask the front desk for your keys
- In TypeScript:
params.get("sort") - In real life: The desk hands you the keys, or says nothing was left
- In TypeScript: It gives a string, or
null - In real life: A friend reminds you: they might not be there
- In TypeScript: TypeScript says
'sort' is possibly 'null' - In real life: You look in your hand before you walk to the car
- In TypeScript: You check
sortbefore you call.toUpperCase()
A value that might be missing has null or undefined in its type, like string | null. With strictNullChecks on, TypeScript won't let you use it as a string until you've handled the missing case: with a check, a backup, or ?.. When the code runs, null and undefined are the same JavaScript values as always. TypeScript only makes sure you planned for them.
strictNullChecks: null gets its own type
Whether TypeScript keeps null in a type depends on one setting, strictNullChecks. Settings like this live in a project's tsconfig.json file. Turning on strict turns it on, along with a group of other checks, and a new project made with tsc --init has strict on:
{
"compilerOptions": {
"strict": true
}
}Line 3 turns on strict, and with it strictNullChecks. Every example in this lesson is checked with it on, unless it says otherwise. Switch it off and on for the overview's code:
const params = new URLSearchParams("?q=tea");const sort = params.get("sort");// ^?console.log(sort.toUpperCase());string | null1 error: line 4throws a TypeErrorThe flag is on, as strict: true sets it. The query on line 3 shows string | null: sort may be text, or it may be null. Line 4 gets 'sort' is possibly 'null', so you find the problem now.
With the flag on, the query on line 3 shows string | null, and line 4 has its error. With the flag off, the query shows only string, and the error is gone. The code throws the same TypeError either way: turning the flag off removes the warning, not the bug. TypeScript's own documentation recommends keeping it on.
With the flag on, null and undefined are types of their own. A plain string can't hold either one, so a type has to say when something may be missing:
let nickname: string = null;let middleName: string | null = null;let lastName: string | null = undefined;Line 1 says nickname is a string and gives it null. TypeScript says Type 'null' is not assignable to type 'string': null isn't allowed in a plain string. Line 2 says middleName is string | null, so null is fine. Line 3 gives that same type undefined and gets Type 'undefined' is not assignable to type 'string | null'. So null and undefined are two different types: a type that allows one doesn't allow the other.
null fits in any type, so nothing warns you. With it on, a plain string holds only text, and null goes only where the type says so, like string | null.You rarely write | null yourself at first. The browser's own functions, and your own data types, already say it. Read the queries, or point at each name:
type CartItem = { name: string; price: number };const cart: CartItem[] = [{ name: "Tea", price: 3 }]; const searchBox = document.querySelector("input");// ^?const theme = localStorage.getItem("theme");// ^?const cake = cart.find((item) => item.name === "Cake");// ^?Line 4 asks the page for an <input> element, and the query on line 5 shows HTMLInputElement | null. Line 6 reads a saved setting from the browser's storage (see Web storage), and the query on line 7 shows string | null. Line 8 searches the cart with find (see Searching arrays), and the query on line 9 shows CartItem | undefined. This example only shows types, because localStorage needs a real site, so it has no Edit & run.
The table lists where missing values come from in everyday frontend code. Read each row as: this code, the type TypeScript gives it, and when the value is missing.
| The code | Its type | When is it missing? |
|---|---|---|
document.querySelector("input") | HTMLInputElement | null | It gives null when no element on the page matches. |
params.get("sort") | string | null | It gives null when the address has no sort. |
localStorage.getItem("theme") | string | null | It gives null when nothing was saved under that name. |
cart.find(…) | CartItem | undefined | It gives undefined when no item matches. |
user.nickname, from nickname?: string | string | undefined | It gives undefined when the object leaves the property out. |
To use a value like this, check it first:
const params = new URLSearchParams("?q=tea");const sort = params.get("sort"); if (sort !== null) { console.log(sort.toUpperCase()); // ^?} else { console.log("No sort chosen");}Line 4 checks that sort isn't null. Inside the braces, the query on line 6 shows const sort: string: TypeScript knows the check passed, so line 5 is fine. Line 8 runs when sort is null, and here it prints No sort chosen. This is the narrowing you met in Union types: a check makes the type smaller. The Narrowing lesson covers it in depth.
Reading safely with ?. and ??
JavaScript has two operators for values that may be missing (the Nullish coalescing and optional chaining lesson covers them):
?.reads a property only when the value before it isn'tnullorundefined. Otherwise it stops and givesundefined.??gives a backup when the value on its left isnullorundefined.
TypeScript understands both and works out the types they give. Here is a user profile where the address is optional:
type Profile = { name: string; address?: { city: string } }; const asha: Profile = { name: "Asha" };console.log(asha.address.city);Line 1 describes a Profile. The ? in address?: makes the property optional: a profile may leave it out. Line 3 makes Asha's profile with no address. Line 4 reads asha.address.city, and TypeScript says 'asha.address' is possibly 'undefined'. It's right: when this runs, it throws a TypeError.
type Profile = { name: string; address?: { city: string } }; const asha: Profile = { name: "Asha" };const city = asha.address?.city;// ^?const label = city ?? "No city yet";// ^?console.log(label);Line 4 reads with ?. instead. Asha has no address, so ?. stops and gives undefined. The query on line 5 shows const city: string | undefined: TypeScript added undefined to the type, because ?. can give it. Line 6 uses ?? to swap a missing city for "No city yet". The query on line 7 shows const label: string: the backup takes the place of undefined, so the type has none left. Line 8 prints No city yet.
asha.address?.city. Asha has no address, so ?. stops and gives undefined. Line 6's ?? swaps that for the backup, "No city yet".?? isn't the same as ||. || also replaces 0, "", and false, which can be real values. TypeScript can't catch this mix-up, because both give a number here:
type Settings = { fontSize?: number }; const settings: Settings = { fontSize: 0 };const withOr = settings.fontSize || 16;const withNullish = settings.fontSize ?? 16;// ^?console.log(withOr, withNullish);Line 3 sets a font size of 0. Line 4 uses ||, which treats 0 as missing and gives 16. Line 5 uses ??, which keeps 0. The query on line 6 shows const withNullish: number, and withOr is a number too, so there's no error either way. Line 7 prints 16 0. When 0 or empty text is a real value, use ??.
The non-null assertion !
Sometimes you know more than TypeScript. A search page's own HTML has a search box, so asking for it always finds one. But TypeScript can't see your HTML:
const searchBox = document.querySelector("input");// ^?console.log(searchBox.value);Line 1 asks the page for its <input>. The query on line 2 shows const searchBox: HTMLInputElement | null, because some page might have none. Line 3 reads the box's value and gets 'searchBox' is possibly 'null'. This page does have a box, with tea typed in it, so when it runs the code prints tea anyway.
A ! right after a value is the non-null assertion. It tells TypeScript: "this isn't null or undefined, trust me".
const searchBox = document.querySelector("input")!;// ^?console.log(searchBox.value);Line 1 ends with !. The query on line 2 shows const searchBox: HTMLInputElement: null is gone from the type. Line 3 has no error and prints tea.
The ! is only a promise. Nothing checks it when the code runs. Make the same promise about the sort order, which really is missing:
const params = new URLSearchParams("?q=tea");const sort = params.get("sort")!;// ^?console.log(sort.toUpperCase());Line 2 ends with !, so the query on line 3 shows const sort: string, and line 4 has no error. When it runs, sort is null, and line 4 throws the same TypeError as the overview. Here is what the browser gets:
! in search.ts, and TypeScript stops asking. The ! is removed with the types, so search.js reads sort with no check, and a missing sort throws a TypeError.const params = new URLSearchParams("?q=tea");
const sort = params.get("sort");
console.log(sort.toUpperCase());It's the overview's JavaScript, line for line. The ! was removed with the types, so it couldn't stop anything. Use ! only when you're sure, like an element your own HTML always has. When you're not sure, check the value or give a backup.
The table compares four ways to handle a value that might be missing. Read each row as: what you write, what TypeScript does, and what happens when the value really is missing.
| You write | What TypeScript does | When the value is missing |
|---|---|---|
if (sort !== null) { … } | Inside the braces, it knows sort is a string. | The other branch runs. Nothing throws. |
sort ?? "relevance" | The result is a string: the backup takes the place of null. | You get the backup, "relevance". |
sort?.toUpperCase() | The result is string | undefined. | You get undefined. Nothing throws. |
sort!.toUpperCase() | It takes your word that sort isn't null, and reports nothing. | It throws a TypeError, as if you had written no check at all. |
Optional parameters vs undefined
A function's parameter can be missing too, when a call leaves it out (see Parameters). A ? after a parameter's name makes it optional. That's different from a parameter whose type includes undefined. Swap line 1 between four ways of writing the name parameter, and watch lines 8 and 9:
function greet(name?: string) { const shown = name ?? "guest"; // ^? return `Hello, ${shown}`;} console.log(greet("Asha"));console.log(greet());console.log(greet(undefined));Hello, Asha / Hello, guest / Hello, gueststring | undefinedNo errorsname?: string makes the parameter optional: a call may leave it out. Inside, the query on line 3 shows string | undefined, because a left-out argument is undefined. All three calls are fine.
Line 7 passes "Asha", line 8 passes nothing, and line 9 passes undefined. With name?: string, all three are fine, and the query on line 3 shows string | undefined. With name: string | undefined, line 8 gets Expected 1 arguments, but got 0: the name must be passed, even if it's undefined. With name: string, line 9 is marked too. With name = "guest", a default value, every call is fine again, and inside the function name is a plain string.
The console output never changes: Hello, Asha, then Hello, guest twice. When the code runs, a left-out argument is undefined either way. Only what TypeScript allows changes.
The table sums up the four. Read each row as: what you write, whether a call can leave the name out, and the type of name inside the function.
| You write | Can a call leave it out? | The type of name inside |
|---|---|---|
name?: string | Yes. | string | undefined |
name: string | undefined | No. It must pass something, even undefined. | string | undefined |
name: string | No, and it can't pass undefined either. | string |
name = "guest" | Yes. A left-out name becomes "guest". | string |
Object properties work the same way. nickname?: may be left out of the object, but bio: string | undefined must be written, even when its value is undefined:
type Profile = { name: string; nickname?: string; bio: string | undefined }; const asha: Profile = { name: "Asha", bio: undefined };const ravi: Profile = { name: "Ravi", nickname: "Rav" };Line 1 describes a Profile with an optional nickname and a bio that may be undefined. Line 3 leaves out the nickname and writes bio: undefined, which is fine. Line 4 writes a nickname but leaves out bio, and gets an error that starts Property 'bio' is missing. In plain words: the object has no bio, and the type says it must have one.
A search page that handles a missing value
Back to the search page. The playground has this lesson's five ways to handle sort on line 4, and two page addresses on line 1: one with no sort, and one with sort=price. Try each way with each address.
const params = new URLSearchParams("?q=tea");const sort = params.get("sort"); console.log(sort.toUpperCase());throws a TypeError?q=tea1 error: line 4Line 4 uses sort without a check, so TypeScript says 'sort' is possibly 'null'. With a sort in the address it prints PRICE. Without one, it throws.
With ?q=tea&sort=price, every way prints PRICE, even Use it as is, which still has its error. TypeScript checks every value sort could have, not just the one this address gives. With ?q=tea, Use it as is and Promise with ! both throw a TypeError. Check for null prints No sort chosen, Backup with ?? prints RELEVANCE, and Read with ?. prints undefined, which a shopper shouldn't see. The check and the ?? backup handle both addresses well.
Real data mixes all of this. A profile from a server may leave out a nickname, have no picture yet, and have no address:
type Profile = { name: string; nickname?: string; avatarUrl: string | null; address?: { city: string };}; function profileLine(user: Profile) { const shown = user.nickname ?? user.name; const city = user.address?.city ?? "somewhere"; const avatar = user.avatarUrl ?? "/default-avatar.png"; return `${shown} from ${city} (${avatar})`;} console.log(profileLine({ name: "Asha", avatarUrl: null }));console.log(profileLine({ name: "Ravi", nickname: "Rav", avatarUrl: "/ravi.png", address: { city: "Pune" } }));Lines 1 to 6 describe the profile: nickname and address are optional, and avatarUrl is a string or null. Line 9 shows the nickname, or the name when there's no nickname. Line 10 reads the city with ?. and falls back to "somewhere". Line 11 falls back to a default picture. Line 15 prints Asha from somewhere (/default-avatar.png), and line 16 prints Rav from Pune (/ravi.png). There's no ! anywhere: every missing value has a plan.
let nickname: string = null;let nickname: string | null = null;const box = document.querySelector("input"); box.value = "tea";const box = document.querySelector("input")!; box.value = "tea";const city = [{ city: "Pune" }].find((a) => a.city === "Goa").city;const city = [{ city: "Pune" }].find((a) => a.city === "Goa")?.city;function greet(name: string | undefined) {} greet();function greet(name?: string) {} greet();
Sort each line by what TypeScript says about it, with strictNullChecks on. Each line is checked on its own.
Common mix-ups
Both null and undefined mean "missing" to you, but TypeScript keeps them apart:
function findNickname(name: string): string | null { return name === "Ravi" ? "Rav" : undefined;}Line 1 says findNickname gives back a string or null. Line 2 gives back undefined when the name isn't Ravi, and TypeScript says Type 'undefined' is not assignable to type 'string | null'. Give back null instead, or write the type as string | null | undefined.
- "null and undefined are the same type." No. They're two types.
string | nulldoesn't acceptundefined, andstring | undefineddoesn't acceptnull. - "The
!checks that the value is there." No. It's removed with the types. If the value is missing, the code throws the sameTypeErroras with no check at all. - "Turning strictNullChecks off fixes the error." No. It hides the warning. The value is still
nullwhen the code runs. - "
?.handles the missing value for me." No. It givesundefinedinstead of throwing. You still decide what to show, often with??. - "
??and||do the same thing." No.||also replaces0,"", andfalse.??replaces onlynullandundefined. - "
name?: stringandname: string | undefinedmean the same." No. Both givenamethe typestring | undefinedinside, but only the first lets a call leave it out.
Practice exercises
Each exercise is TypeScript. When you fix code, press Edit & run on the starter code: the errors update as you type, and the console shows what it prints. Try a guess first, then use the hints.
No type is written here. What type does TypeScript give couponBox? Guess first. Then point at couponBox, or press Edit & run and add a // ^? line under it.
const couponBox = document.querySelector("#coupon");const couponBox = document.querySelector("#coupon");// ^?querySelector gives null when nothing matches, so the type is Element | null. With a tag name like "input" TypeScript knows the exact element type; with an id like #coupon it only knows it's some Element.
A user with an optional nickname. Which line will TypeScript mark?
type User = { name: string; nickname?: string };
const asha: User = { name: "Asha" };
console.log(asha.nickname.toUpperCase());type User = { name: string; nickname?: string }; const asha: User = { name: "Asha" };console.log(asha.nickname?.toUpperCase() ?? asha.name);Line 4 calls .toUpperCase() on asha.nickname, which is string | undefined, so TypeScript says 'asha.nickname' is possibly 'undefined'. The fix reads it with ?. and falls back to her name, so it prints Asha.
TypeScript marks line 4. Fix it so the theme is "light" when none is saved. What does it print?
type Settings = { theme?: string }; const settings: Settings = {};const theme: string = settings.theme;console.log(theme);type Settings = { theme?: string }; const settings: Settings = {};const theme: string = settings.theme ?? "light";console.log(theme);settings.theme ?? "light" gives the theme, or "light" when it's undefined. Its type is string, so line 4 is fine, and the code prints light.
Line 6 leaves out the currency, and TypeScript marks it. Change line 1 so a left-out currency means EUR. What does priceLabel(3) print then?
function priceLabel(price: number, currency: string) { return `${price} ${currency}`;} console.log(priceLabel(3, "USD"));console.log(priceLabel(3));function priceLabel(price: number, currency = "EUR") { return `${price} ${currency}`;} console.log(priceLabel(3, "USD"));console.log(priceLabel(3));A default value makes currency optional, and inside it's always a string. Line 5 still prints 3 USD, and line 6 now prints 3 EUR. currency?: string with ${currency ?? "EUR"} works too.
TypeScript reports nothing here. What happens when it runs on a page with no #total? Then remove the ! and print No total on this page instead.
const totalBox = document.querySelector("#total")!;totalBox.textContent = "$7";console.log("Total shown");const totalBox = document.querySelector("#total");if (totalBox !== null) { totalBox.textContent = "$7"; console.log("Total shown");} else { console.log("No total on this page");}With no #total on the page, totalBox is null, and the starter throws a TypeError at line 2: the ! hid the problem. Without the !, totalBox is Element | null, and the check makes TypeScript happy and the page safe. It prints No total on this page.
A profile may have no address, and an address may have a null city. Fix line 4 so every user gets a label. What does your code print for Mei?
type Profile = { name: string; address?: { city: string | null } }; function cityLabel(user: Profile) { return user.address.city.toUpperCase();} console.log(cityLabel({ name: "Asha", address: { city: "Pune" } }));console.log(cityLabel({ name: "Ravi", address: { city: null } }));console.log(cityLabel({ name: "Mei" }));type Profile = { name: string; address?: { city: string | null } }; function cityLabel(user: Profile) { return user.address?.city?.toUpperCase() ?? "Unknown city";} console.log(cityLabel({ name: "Asha", address: { city: "Pune" } }));console.log(cityLabel({ name: "Ravi", address: { city: null } }));console.log(cityLabel({ name: "Mei" }));user.address?.city?.toUpperCase() stops at a missing address or a null city and gives undefined, and ?? swaps that for "Unknown city". It prints PUNE, then Unknown city for Ravi and for Mei.
Check your understanding
For each question, keep two moments apart: what TypeScript says before the code runs, and what the code does when it runs. The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 7What type does TypeScript give
params.get("sort")?Choose an answer to see the explanation.
Question 2 of 7With strictNullChecks off, what happens with this code?
Read the code, then predictconst params = new URLSearchParams("?q=tea"); const sort = params.get("sort"); console.log(sort.length);Choose an answer to see the explanation.
Question 3 of 7What does Ravi's city line print?
Read the code, then predicttype Profile = { name: string; address?: { city: string } }; const ravi: Profile = { name: "Ravi" }; console.log(ravi.address?.city ?? "No city");Choose an answer to see the explanation.
Question 4 of 7What does this order line print, with a quantity of 0?
Read the code, then predicttype Order = { item: string; quantity?: number }; const order: Order = { item: "Tea", quantity: 0 }; console.log(order.quantity || 1, order.quantity ?? 1);Choose an answer to see the explanation.
Question 5 of 7Which line does TypeScript flag in these greetings?
Read the code, then predictfunction greet(name: string | undefined) { return `Hello, ${name ?? "guest"}`; } greet("Asha"); greet(undefined); greet();Choose an answer to see the explanation.
Question 6 of 7What does the
!insort!.toUpperCase()do when the code runs?Choose an answer to see the explanation.
Question 7 of 7Does TypeScript accept this middle name?
Read the code, then predictlet middleName: string | null = undefined;Choose an answer to see the explanation.
Key takeaways
- A value that might be missing has
nullorundefinedin its type, likestring | null. - With
strictNullCheckson (part ofstrict), TypeScript makes you handle the missing case before you use the value. Off, the bug is still there; only the warning is gone. nullandundefinedare two different types. Browser functions likequerySelectorgivenull;findand optional properties giveundefined.- Check with
if (x !== null), read with?., and give a backup with??. TypeScript follows each one. !removesnullandundefinedfrom the type and checks nothing. Use it only when you're sure.x?: Tmay be left out;x: T | undefinedmust be written, even asundefined.
Remember the one-liner.
If a value can be missing, its type says so, and TypeScript won't let you forget it.
Coming next: Enums. They give a fixed set of choices names, like the sort orders a search page offers, and they're one of the few TypeScript features that still exist when the code runs.