JavaScript with types: catch mistakes before your code runs.
From your first line of code to computing types with types. The course teaches programming and JavaScript from zero (values, conditions, loops, functions, objects, modules and promises), then adds TypeScript: everyday types, unions and narrowing, generics, types derived from values, mapped, conditional and template literal types, soundness and configuration. Every other course builds on it.
What you’ll be able to do
Write and run JavaScript programs: variables, conditions, loops, functions, arrays, objects, modules and async code.
Explain what the TypeScript checker does, what it erases, and where run-time checks have to take over.
Model data with unions and discriminated unions, and let narrowing and never prove every case handled.
Derive types from values with typeof, keyof, as const and satisfies instead of writing them twice.
Read and write generics, mapped, conditional and template literal types, including the ones inside libraries.
Spot unsound code, keep escape hatches honest, configure tsconfig, and test your types.
Modules
Your first program· 8 slides
Values and variables· 9 slides
Decisions and loops· 8 slides
Functions· 9 slides
Arrays, objects and missing values· 12 slides
Modules, errors and waiting· 10 slides
Meet TypeScript· 10 slides
Everyday types· 9 slides
Unions and narrowing· 9 slides
Shapes, classes and escape hatches· 9 slides
Typing async code, data and errors· 9 slides
Generics· 9 slides
Types from values· 9 slides
Utility types and mapped types· 9 slides
Conditional types and infer· 8 slides
Template literal types· 9 slides
Soundness, brands and type tests· 10 slides
tsconfig, modules and declarations· 10 slides
Type-level programming· 10 slides
Mastery challenge· 13 slides
Interactive parts (exercises, code to run, walkthroughs, flashcards, checklists) are drawn still and marked “Interactive · disabled in the PDF”. Do them in the app; the answers are at the end.
Module 1: Your first program
TypeScript1 / 189
Module 1 · Your first program
A program is a list of instructions
A program is a list of instructions that a computer follows, one after another. A recipe is a good picture of one: "boil the water, add the pasta, wait ten minutes". The difference is that a computer does exactly what the words say, nothing more, and never guesses what you meant.
You write those instructions as code: text in a programming language, a language with strict rules that a computer can follow. This course starts with JavaScript, the language of the web, and then adds TypeScript on top of it.
Program
Instructions a computer follows, in order, to do a job.
Code
The text of a program, written in a programming language.
Programming language
A language with exact rules, so a computer can follow it without guessing.
No experience needed. This course assumes you have never written code. Every new word is explained the first time it appears, and every module ends with flashcards for the words it introduced.
TypeScript2 / 189
Module 1 · Your first program
JavaScript runs in browsers and on servers
Code needs something to run it. That something is called a runtime. JavaScript has two common ones: every web browser (Chrome, Firefox, Safari, Edge) has one built in, and Node.js is one you install to run JavaScript on your own computer or on a server.
The same language, run by different runtimes.
Runtime
The program that reads your code and carries it out.
Browser
The app you use to visit websites. It can run JavaScript.
Node.js
A runtime for JavaScript outside the browser. Often just "Node".
Server
A computer that answers requests from other computers, such as websites.
TypeScript3 / 189
Module 1 · Your first program
Your first line: console.log
console.log(...) prints whatever you put between the brackets. It is how you will see what your code is doing for the rest of this course.
WalkthroughInteractive · disabled in the PDF
JavaScript
1console.log('Welcome to the library')
Lines 1:console is a tool the runtime gives you for printing messages. In a browser it is the console panel; in Node it is the terminal.
Lines 1:.log picks one of its abilities: "write this down". The dot means "the log that belongs to console".
Lines 1: The brackets ( ) say "do it now, with this". What is inside, 'Welcome to the library', is a piece of text. The quotes mark where the text starts and ends; they are not printed.
Predict the outputInteractive · disabled in the PDF
What does this print?
JavaScript
1console.log('Books: 3')
What does this print?
TypeScript4 / 189
Module 1 · Your first program
Three places to run code today
Where
How to open it
Good for
The browser console
In Chrome, press F12 (or Ctrl+Shift+J, Cmd+Option+J on a Mac) and pick Console
Trying one line at a time, with nothing to install
JavaScript and TypeScript in a page, with errors shown as you type
Node.js
Install it from nodejs.org, then run files from a terminal
Real projects
Use the playground for this course. It runs JavaScript, shows TypeScript errors as you type, and you cannot break anything in it. Press Run and the result of console.log appears in the Logs panel on the right.
Try itInteractive · disabled in the PDF
TypeScript5 / 189
Module 1 · Your first program
Instructions run from top to bottom
A program with several lines runs them in order, from the first to the last. Each finished instruction is called a statement. A line that starts with // is a comment: a note for people, which the runtime skips.
JavaScript
1// Opening the library in the morning2console.log('Unlock the door')3console.log('Turn on the lights')4console.log('Open the catalogue')
Three statements, printed in the order they are written.
Put in orderInteractive · disabled in the PDF
A program checks out a library book in three statements. Put them in the order they should run.
1.console.log('Scan the book')
2.console.log('Scan the card')
3.console.log('Print the receipt')
PlaygroundInteractive · disabled in the PDF
JavaScript
1// Closing the library at night2console.log('Switch off the lights')3console.log('Lock the door')4console.log('Go home')
Press Run. Then swap two lines, add one of your own, or put // in front of one, and run it again.
TypeScript6 / 189
Module 1 · Your first program
The terminal runs programs by name
A terminal (also called a command line or shell) is a window where you type commands instead of clicking. Real projects are run from one. Each line you type is a command, and the terminal answers below it.
Terminal
1node--version# prints the version of Node you have2nodehello.js# runs the JavaScript in the file hello.js
In these examples, everything after # is a comment, not part of the command.
Terminal
A window for typing commands to your computer.
Command
One instruction typed into a terminal, run when you press Enter.
File
A named piece of saved text, like hello.js. JavaScript files end in .js.
Tip. Opening a terminal: on Windows, search for Terminal; on a Mac, Terminal is in Applications, Utilities; on Linux it is usually Ctrl+Alt+T. Code editors such as VS Code have one built in.
TypeScript7 / 189
Module 1 · Your first program
An error tells you what, and where
When the runtime cannot follow an instruction, it stops and prints an error. Errors are not a sign you did badly; they are the computer telling you precisely what confused it. Read them from the start: the kind of error, then the message, then where it happened.
Text
1hello.js:22consle.log('Turn on the lights')3^45ReferenceError: consle is not defined
Line 2 of hello.js; a ReferenceError; the name consle means nothing to JavaScript.
Find the bugInteractive · disabled in the PDF
The runtime stops with "ReferenceError: Console is not defined". Which line is wrong?
JavaScript
TypeScript8 / 189
Module 1 · Your first program
Recap
A program is instructions run in order, top to bottom.
JavaScript runs in browsers and in Node.js; both are runtimes.
console.log(...) prints a value so you can see what the code does.
The playground, the browser console and a terminal with Node can all run it.
An error names its kind, its message and the line where it happened. Read it before changing anything.
FlashcardsInteractive · disabled in the PDF
Program
A list of instructions a computer follows, in order.
Runtime
What reads and carries out your code: a browser or Node.js.
Statement
One complete instruction in a program.
Comment
Text after // that the runtime ignores: a note for people.
Terminal
A window where you type commands, such as node hello.js.
Case-sensitive
console and Console are different names.
Module 2: Values and variables
TypeScript9 / 189
Module 2 · Values and variables
Every value has a kind
A value is a piece of data a program works with: a book title, a number of copies, whether a book is on loan. JavaScript has a few basic kinds of value, and the three you will use most are strings (text), numbers and booleans (true or false).
JavaScript
1console.log('The Hobbit')// a string: text in quotes2console.log(310)// a number: no quotes3console.log(true)// a boolean: true or false4console.log(typeof310)// prints: number
typeof tells you which kind a value is.
Value
One piece of data: 310, true, 'The Hobbit'.
String
Text, written between quotes. Single '…' and double "…" quotes do the same thing.
Number
Any number, whole or with a decimal point: 3, -2, 4.5.
Boolean
One of two values, true or false. Used for yes-or-no facts.
QuizInteractive · disabled in the PDF
What kind of value is '310', with quotes?
What kind of value is '310', with quotes?
TypeScript10 / 189
Module 2 · Values and variables
A variable is a name for a value
A variable gives a value a name, so you can use it later without writing it again. You declare one with const (or let), a name, =, and the value. Here = does not mean "equals"; it means "store this value under this name".
WalkthroughInteractive · disabled in the PDF
JavaScript
1const title ='The Hobbit'2const pages =3103console.log(title)4console.log(pages)
Lines 1: Create a variable named title and store the string 'The Hobbit' in it.
Lines 2: Create pages and store the number 310.
Lines 3-4: Using a name gives back its value: these print The Hobbit and 310. No quotes around title: with quotes it would be the text "title".
Variable
A name that holds a value.
Declare
Create a variable, with const or let.
Assign
Store a value in a variable with =.
TypeScript11 / 189
Module 2 · Values and variables
const never changes, let can
A const variable keeps the value it started with: assigning to it again is an error. A let variable can be given a new value later. Use const by default, and let only for values that really change, such as a counter.
The right side is worked out first (3 minus 1), then stored back in the variable.
Find the bugInteractive · disabled in the PDF
This stops at line 3 with "TypeError: Assignment to constant variable." Which line has to change to fix it?
JavaScript
TypeScript12 / 189
Module 2 · Values and variables
Numbers do arithmetic
An operator is a symbol that works on values. The arithmetic ones are +, -, * (times), / (divided by) and % (the remainder after dividing). As in school maths, * and / happen before + and -, and brackets go first of all.
Code
Result
Why
10 + 4
14
Add
10 / 4
2.5
Division keeps the decimals
10 % 4
2
10 is two fours with 2 left over
2 + 3 * 4
14
Times first: 3 × 4, then add 2
(2 + 3) * 4
20
Brackets first
Predict the outputInteractive · disabled in the PDF
A late fee is 2 per day plus a fixed 5. What does this print?
JavaScript
1const daysLate =32console.log(5+ daysLate *2)
A late fee is 2 per day plus a fixed 5. What does this print?
TypeScript13 / 189
Module 2 · Values and variables
Joining text, and template strings
With strings, +joins them end to end. Joining a string and a number turns the number into text first, which surprises everyone once. For building text out of values, a template string is clearer: it uses backticks instead of quotes, and ${...} drops a value into the text.
JavaScript
1const title ='The Hobbit'2const copies =33console.log(title +' by Tolkien')// The Hobbit by Tolkien4console.log('Copies: '+ copies)// Copies: 35console.log(`${title}: ${copies} left`)// The Hobbit: 3 left
Predict the outputInteractive · disabled in the PDF
What does this print?
JavaScript
1console.log('2'+2)
What does this print?
PlaygroundInteractive · disabled in the PDF
JavaScript
1const title ='Emma'2const author ='Jane Austen'3const pages =47445console.log(title +' by '+ author)6console.log(`${title} has ${pages} pages`)7console.log(`Twice as long: ${pages *2} pages`)8console.log('2'+2,2+2)
Change the book, or put a calculation inside ${...}, and run it again.
TypeScript14 / 189
Module 2 · Values and variables
Try it: build a sentence
Your turn. The variables are set up; build two strings from them. The tests check the exact text, spaces included.
Code challengeInteractive · disabled in the PDF
Set fullName to the first name, a space and the last name ('Ada Lovelace'), and summary to 'Ada Lovelace has 3 books', using the variables rather than typing the names.
JavaScript
1const first ='Ada'2const last ='Lovelace'3const books =345const fullName =''6const summary =''
TypeScript15 / 189
Module 2 · Values and variables
Comparisons give booleans
Comparison operators ask a question and answer true or false. === asks "is it the same value?", !== asks "is it different?", and <, >, <=, >= compare sizes.
JavaScript
1const copies =02console.log(copies ===0)// true3console.log(copies >0)// false4console.log('3'===3)// false: a string is not a number
Use three equals signs. JavaScript also has ==, which converts values before comparing, so '3' == 3 is true. Those conversions have odd rules. Always use === and !==; one = is assignment, not comparison.
Fill the blanksInteractive · disabled in the PDF
Fill in the operators: store 5 in fine, then check whether it is more than 3.
JavaScript
1const fine 52console.log(fine 3)// true
TypeScript16 / 189
Module 2 · Values and variables
Good names make code readable
Variable names can use letters, digits, _ and $, but cannot start with a digit or contain spaces. JavaScript code uses camelCase: words joined together, each after the first starting with a capital, like copiesOnShelf. Pick names that say what the value means, not what kind it is.
Hard to read
const x ='The Hobbit'const n =3const b =true
Says what it means
const title ='The Hobbit'const copies =3const isOnLoan =true
Note. You may see a ; at the end of statements in other code. In JavaScript it is mostly optional; this course leaves it out. Older code also uses var instead of let: it behaves differently in small ways, and modern code does not need it.
TypeScript17 / 189
Module 2 · Values and variables
Recap
Strings are text in quotes, numbers have no quotes, booleans are true or false.
const names a value that stays; let names one that changes.
* and / happen before + and -; brackets go first.
+ with a string joins text. Template strings drop values into text with ${...}.
Compare with === and !==, never ==.
FlashcardsInteractive · disabled in the PDF
Value
One piece of data, such as 310 or true.
Variable
A name that holds a value.
const or let?
const unless the value has to change.
Operator
A symbol that works on values: +, *, ===.
Template string
Text in backticks that can include values with ${...}.
%
The remainder after division: 10 % 4 is 2.
camelCase
Joined words with capitals after the first: copiesOnShelf.
Module 3: Decisions and loops
TypeScript18 / 189
Module 3 · Decisions and loops
if runs code only when a condition is true
So far every line ran. An if statement runs a group of lines only when a condition (an expression that gives a boolean) is true. The lines it controls go in braces{ }, a block, and are indented so you can see they belong together. else holds what to do otherwise.
WalkthroughInteractive · disabled in the PDF
JavaScript
1const copiesOnShelf =023if(copiesOnShelf >0){4 console.log('You can borrow it now')5}else{6 console.log('Join the waiting list')7}
Lines 3: Work out the condition in the brackets: 0 > 0 is false.
Lines 4: This block runs only when the condition is true, so it is skipped.
Lines 5-7: The else block runs instead: it prints Join the waiting list.
Condition
Something that is either true or false, like copies > 0.
Block
Lines grouped in braces { }, run together.
Expression
Any piece of code that produces a value: 3, copies > 0, a + b.
TypeScript19 / 189
Module 3 · Decisions and loops
else if chooses between several cases
Chain else if to test one condition after another. JavaScript checks them in order and runs the first block whose condition is true, then skips the rest.
JavaScript
1const daysLate =1223if(daysLate ===0){4 console.log('On time')5}elseif(daysLate <7){6 console.log('A little late')7}elseif(daysLate <30){8 console.log('Late: a fee applies')9}else{10 console.log('Very late: the book is marked lost')11}
QuizInteractive · disabled in the PDF
With daysLate set to 3, what does the code above print?
With daysLate set to 3, what does the code above print?
Not a member, but has a day pass: false || true is true, so it prints.
Fill the blanksInteractive · disabled in the PDF
A visitor may borrow if they are a member and do not have fines.
JavaScript
1if(isMember hasFines){2 console.log('You may borrow books')3}
TypeScript21 / 189
Module 3 · Decisions and loops
Any value can act as a condition
A condition does not have to be a boolean. JavaScript treats a handful of values as false, called falsy: false, 0, '' (the empty string), null, undefined and NaN. Everything else is truthy, including '0' and 'false', because they are non-empty strings.
Predict the outputInteractive · disabled in the PDF
Note.null, undefined and NaN ("not a number", the result of maths that makes no sense) come up properly in a later module. For now, remember that they are falsy. (Two rarer zeros, -0 and 0n, are falsy too.)
TypeScript22 / 189
Module 3 · Decisions and loops
switch compares one value against many
When you compare one value with several possible answers, switch reads better than a long else if chain. Each case is compared with ===. break ends the switch; without it, the code carries on into the next case. default runs when no case matches.
JavaScript
1const status ='on-loan'23switch(status){4case'available':5 console.log('On the shelf')6break7case'on-loan':8 console.log('Borrowed by a member')9break10default:11 console.log('Unknown status')12}
Find the bugInteractive · disabled in the PDF
With status set to available, this prints both "On the shelf" and "Borrowed by a member". Which line is missing something?
JavaScript
TypeScript23 / 189
Module 3 · Decisions and loops
A for loop repeats code a set number of times
A loop runs a block again and again. The for loop is the classic one: it has three parts in its brackets, separated by ;: a starting step, a condition checked before each round, and a step that runs after each round. One round is called an iteration.
Lines 1: Start: create a counter, shelf, set to 1.
Lines 1: Before each iteration, check shelf <= 3. While it is true, run the block; when it becomes false, stop.
Lines 2: The block prints Checking shelf 1, then 2, then 3.
Lines 1: After each iteration, shelf = shelf + 1 moves the counter on. You will often see this written shelf++, which means the same.
Predict the outputInteractive · disabled in the PDF
How many lines does this print?
JavaScript
1for(let i =0; i <4; i++){2 console.log(i)3}
How many lines does this print?
PlaygroundInteractive · disabled in the PDF
JavaScript
1let total =02for(let day =1; day <=5; day++){3 total = total +24 console.log(`Day ${day}: fee so far ${total}`)5}6console.log(`Final fee: ${total}`)
Make it count to 10, or charge 3 a day. What happens if you start day at 0?
TypeScript24 / 189
Module 3 · Decisions and loops
A while loop repeats until a condition is false
while is a simpler loop: only a condition. It suits work where you do not know in advance how many rounds it takes. Something in the block must eventually make the condition false, or the loop never ends: an infinite loop, which freezes the page or program.
JavaScript
1let waiting =32while(waiting >0){3 console.log(`Serving a member, ${waiting} in the queue`)4 waiting = waiting -15}6console.log('Queue empty')
Find the bugInteractive · disabled in the PDF
This loop never stops. Which line is wrong?
JavaScript
TypeScript25 / 189
Module 3 · Decisions and loops
Recap
if/else if/else runs the first block whose condition is true.
&& is and, || is or, ! is not.
false, 0, '', null, undefined and NaN are falsy; everything else is truthy.
switch compares one value with === against many cases; end each with break.
for repeats with a counter; while repeats until its condition is false. Make sure it becomes false.
FlashcardsInteractive · disabled in the PDF
Condition
An expression that is true or false, which decides what runs.
Block
Lines inside braces { }, run together.
Falsy
Values treated as false: false, 0, '', null, undefined, NaN.
Fall-through
A switch case without break runs on into the next case.
Iteration
One round of a loop.
Infinite loop
A loop whose condition never becomes false.
Module 4: Functions
TypeScript26 / 189
Module 4 · Functions
A function is a named, reusable piece of work
A function is a group of statements with a name. Writing it, called defining it, does not run anything yet. Using its name followed by brackets calls it: runs its statements. Define once, call as many times as you like.
JavaScript
1functionopenLibrary(){2 console.log('Unlock the door')3 console.log('Turn on the lights')4}56openLibrary()// runs both lines7openLibrary()// and again, the next morning
function, a name, (), then the body in braces. The last two lines call it.
Function
A named group of statements that runs when called.
Define
Write a function. Nothing runs yet.
Call
Run a function: its name followed by (). console.log(...) is a call too.
Body
The statements inside the function’s braces.
TypeScript27 / 189
Module 4 · Functions
Parameters are the inputs
A function can take inputs. In the definition they are parameters: names listed in the brackets, which act like variables inside the body. In a call, the values you pass are arguments. They are matched by position: the first argument goes to the first parameter.
WalkthroughInteractive · disabled in the PDF
JavaScript
1functiongreet(name, booksOnLoan){2 console.log(`Hello ${name}, you have ${booksOnLoan} books`)3}45greet('Ada',2)6greet('Grace',0)
Lines 1: Two parameters: name and booksOnLoan.
Lines 2: The body uses them like ordinary variables.
Lines 5: This call runs the body with name set to 'Ada' and booksOnLoan set to 2.
Lines 6: Same body, different arguments: Hello Grace, you have 0 books.
Predict the outputInteractive · disabled in the PDF
What does this print?
JavaScript
1functiondescribe(title, pages){2 console.log(`${title} has ${pages} pages`)3}4describe(310,'The Hobbit')
What does this print?
TypeScript28 / 189
Module 4 · Functions
return hands a value back
return ends the function and hands a value back to whoever called it: the return value. The call itself then stands for that value, so you can store it, print it or use it in a calculation. Printing and returning are different: console.log shows a value to you, return gives it to the rest of the program.
JavaScript
1functionlateFee(daysLate){2return daysLate *23}45const fee =lateFee(4)// fee is 86console.log(fee +lateFee(1))// 10
Predict the outputInteractive · disabled in the PDF
What does this print?
JavaScript
1functiondouble(n){2 console.log(n *2)3}4const result =double(5)5console.log(result)
What does this print?
TypeScript29 / 189
Module 4 · Functions
Arrow functions are a shorter way to write one
An arrow function is written with => and usually stored in a const. With braces it works like any function body and needs return. Without braces, the single expression after the arrow is returned automatically.
JavaScript
1functionlateFeeA(days){2return days *23}45constlateFeeB=(days)=>{6return days *27}89constlateFeeC=(days)=> days *2// no braces: returned1011console.log(lateFeeA(3),lateFeeB(3),lateFeeC(3))// 6 6 6
Three ways to write the same function. console.log can print several values, separated by commas.
Fill the blanksInteractive · disabled in the PDF
Complete an arrow function that adds a fine to a balance.
JavaScript
1constaddFine=(balance, fine) balance fine
Find the bugInteractive · disabled in the PDF
total(2, 3) gives undefined instead of 5. Which line is wrong?
JavaScript
TypeScript30 / 189
Module 4 · Functions
Variables live inside their block
A variable declared inside a block (a function body, an if, a loop) exists only inside that block. That region is its scope. Code inside can see variables from outside it, but not the other way round. This keeps functions from tripping over each other’s names.
JavaScript
1const libraryName ='Town Library'23functionreceipt(title){4const message =`${libraryName}: you borrowed ${title}`5return message6}78console.log(receipt('Emma'))// sees libraryName from outside9console.log(message)// ReferenceError: message is not defined
Predict the outputInteractive · disabled in the PDF
Which of fee, days and total can line 6 use?
JavaScript
1const fee =22functioncharge(days){3const total = fee * days4return total5}6console.log(/* line 6 */)
Which of fee, days and total can line 6 use?
TypeScript31 / 189
Module 4 · Functions
Default values for missing arguments
If a call leaves out an argument, that parameter is undefined. A default value, written = value after the parameter, is used instead when the argument is missing.
JavaScript
1functionloanDays(days =14){2return`Due back in ${days} days`3}45console.log(loanDays())// Due back in 14 days6console.log(loanDays(7))// Due back in 7 days
Tip. Keep functions small and give each one a single job. A good function name is a verb that says what it does: lateFee, isOverdue, formatTitle. A boolean-returning function often starts with is, has or can.
TypeScript32 / 189
Module 4 · Functions
Try it: a late fee
Put parameters, return, a default and an if together. The tests call your function with different arguments and check what it returns.
Code challengeInteractive · disabled in the PDF
Finish lateFee(daysLate, perDay = 2): it returns daysLate * perDay, but 0 when the book is not late (daysLate is 0 or less).
JavaScript
1functionlateFee(daysLate, perDay =2){2return03}
TypeScript33 / 189
Module 4 · Functions
Functions are values too
A function is a value, like a number or a string: you can store it in a variable and pass it to another function as an argument. A function passed in this way is called a callback: the receiving function decides when to call it. This idea is everywhere in JavaScript, starting with lists in the next module.
JavaScript
1functionrepeat(times, action){2for(let i =1; i <= times; i++){3action(i)4}5}67repeat(3,(n)=> console.log(`Reminder ${n}: return your book`))
The arrow function is passed in as action, and repeat calls it three times.
PlaygroundInteractive · disabled in the PDF
JavaScript
1functionrepeat(times, action){2for(let i =1; i <= times; i++){3action(i)4}5}67constremind=(n)=> console.log(`Reminder ${n}`)8repeat(3, remind)9repeat(2,(n)=> console.log(`Shelf ${n} checked`))
Pass a different callback, or change how many times. Try repeat(2, remind()) and read the error.
QuizInteractive · disabled in the PDF
What is the difference between repeat(2, notify) and repeat(2, notify())?
What is the difference between repeat(2, notify) and repeat(2, notify())?
TypeScript34 / 189
Module 4 · Functions
Recap
Define a function once; call it with name(...) whenever you need it.
Parameters are the names in the definition; arguments are the values in a call, matched by position.
return hands a value back. Without it, a call gives undefined.
Arrow functions without braces return their expression; with braces they need return.
Variables exist only inside the block where they are declared.
Functions are values: you can pass one as a callback.
FlashcardsInteractive · disabled in the PDF
Parameter
An input name in a function’s definition.
Argument
The value passed for a parameter when calling.
Return value
What a call hands back with return; undefined if there is none.
Arrow function
(a, b) => a + b: a shorter function, often stored in a const.
Scope
The part of the code where a variable exists.
Callback
A function passed to another function, to be called by it.
Module 5: Arrays, objects and missing values
TypeScript35 / 189
Module 5 · Arrays, objects and missing values
An array is an ordered list
An array holds several values in order, written in square brackets and separated by commas. Each value is an element, and its position is its index. Indexes start at 0, so the first element is [0]. .length is how many elements there are.
JavaScript
1const shelf =['Emma','Dune','Beloved']23console.log(shelf[0])// Emma4console.log(shelf[2])// Beloved5console.log(shelf.length)// 36console.log(shelf[3])// undefined: there is no fourth book
Array
An ordered list of values: [1, 2, 3].
Element
One value in an array.
Index
An element’s position, counted from 0.
QuizInteractive · disabled in the PDF
An array has 5 elements. What is the index of the last one?
An array has 5 elements. What is the index of the last one?
TypeScript36 / 189
Module 5 · Arrays, objects and missing values
Arrays can grow and shrink
push adds an element at the end, pop removes the last one and returns it, and includes asks whether a value is in the list. These are methods: functions that belong to a value, called with a dot.
JavaScript
1const todo =['Shelve returns','Order new books']2todo.push('Fix the printer')3console.log(todo.length)// 34const last = todo.pop()// 'Fix the printer'5console.log(todo.includes('Order new books'))// true
const and arrays.todo is a const, yet its contents change. const only stops the name from pointing at a different array; the array itself can still be changed. The same goes for objects, on the next slides.
TypeScript37 / 189
Module 5 · Arrays, objects and missing values
for...of visits every element
To do something with each element in turn, use for...of. On every iteration, the variable you name holds the next element. No counter, no index, no off-by-one mistakes.
JavaScript
1const returns =['Emma','Dune','Beloved']23for(const title of returns){4 console.log(`Shelving ${title}`)5}
Fill the blanksInteractive · disabled in the PDF
Add up the pages of every book.
JavaScript
1const pages =[310,412,324]2let total =03for(const p pages){4 total p5}
TypeScript38 / 189
Module 5 · Arrays, objects and missing values
map, filter and find take a callback
Three array methods do most everyday work, each by calling your callback on every element. map makes a new array of the callback’s results. filter keeps the elements for which the callback returns true. find returns the first element that matches, or undefined if none does.
JavaScript
1const pages =[310,412,96]23const minutes = pages.map((p)=> p *2)// [620, 824, 192]4const long = pages.filter((p)=> p >300)// [310, 412]5const short = pages.find((p)=> p <100)// 966const tiny = pages.find((p)=> p <10)// undefined
map and filter return new arrays and leave the original as it was.
Predict the outputInteractive · disabled in the PDF
What does this print?
JavaScript
1const days =[0,3,10,1]2const fees = days.filter((d)=> d >2).map((d)=> d *2)3console.log(fees)
What does this print?
TypeScript39 / 189
Module 5 · Arrays, objects and missing values
Try it: fees for late books
Chain filter and map. Each number in the list is how many days late a book is; books on time have 0 or less.
Code challengeInteractive · disabled in the PDF
Finish lateFees(days): keep only the books that are late (more than 0 days), and return the fee for each, 2 per day, in the same order.
JavaScript
1functionlateFees(days){2return days3}
TypeScript40 / 189
Module 5 · Arrays, objects and missing values
An object groups named values
An object describes one thing with named fields called properties. Each property is a key (its name) and a value. You write objects in braces, and read a property with a dot, or with square brackets when the key is in a variable.
JavaScript
1const book ={2title:'Dune',3author:'Frank Herbert',4pages:412,5onLoan:false6}78console.log(book.title)// Dune9const field ='pages'10console.log(book[field])// 41211book.onLoan =true// properties can be changed12book.year =1965// or added
Object
A value made of named properties: { title: 'Dune' }.
Property
One named value in an object.
Key
A property’s name, such as title.
Method
A property whose value is a function, like push on arrays.
TypeScript41 / 189
Module 5 · Arrays, objects and missing values
Arrays of objects are everyday data
Most real data is an array of objects: a catalogue of books, a list of members, a to-do list. Every lesson so far comes together here.
Lines 7: Keep the books that are not on loan: !book.onLoan is true for them.
Lines 8: Turn each remaining book into its title.
Lines 9: An array of two strings.
QuizInteractive · disabled in the PDF
What does catalogue.find((b) => b.pages > 400).title give, with the catalogue above?
What does catalogue.find((b) => b.pages > 400).title give, with the catalogue above?
TypeScript42 / 189
Module 5 · Arrays, objects and missing values
Try it: total a cart
A bookshop cart is an array of objects: each line has a title, a price and a quantity. Add it up.
Code challengeInteractive · disabled in the PDF
Finish cartTotal(cart): return the sum of price * quantity over every line. An empty cart costs 0.
JavaScript
1functioncartTotal(cart){2let total =03// TODO: add each line's price times its quantity4return total5}
TypeScript43 / 189
Module 5 · Arrays, objects and missing values
undefined and null mean "nothing here"
JavaScript has two values for "no value". undefined is what you get when something was never set: a missing property, an index past the end, a function with no return. null is a value you set on purpose to say "deliberately empty", such as a book with no borrower.
JavaScript
1const book ={title:'Dune',borrower:null}2console.log(book.borrower)// null: nobody, on purpose3console.log(book.isbn)// undefined: never set4console.log(book.isbn.length)5// TypeError: Cannot read properties of undefined (reading 'length')
Reading a property of nothing is the most common error in JavaScript.
Find the bugInteractive · disabled in the PDF
This crashes with "Cannot read properties of undefined (reading 'name')". Which line?
JavaScript
TypeScript44 / 189
Module 5 · Arrays, objects and missing values
?. and ?? deal with missing values
Optional chaining, a?.b, reads b only if a is not null or undefined; otherwise the whole thing is undefined instead of an error. The nullish coalescing operator, a ?? b, gives a unless it is null or undefined, and then gives b: a fallback.
JavaScript
1const member = members.find((m)=> m.id ===2)2console.log(member?.name)// undefined, no crash3console.log(member?.name ??'Guest')// Guest
Predict the outputInteractive · disabled in the PDF
A book has fine: 0. What do these give?
JavaScript
1const a = book.fine ||52const b = book.fine ??5
A book has fine: 0. What do these give?
TypeScript45 / 189
Module 5 · Arrays, objects and missing values
Objects are shared, not copied
A variable holding an object or array holds a reference to it: a pointer to one object somewhere in memory. Assigning it to another variable copies the pointer, not the object, so both names change the same thing. To make a copy, use the spread syntax ..., which copies the properties (or elements) into a new one. It copies one level only: objects inside it are still shared.
JavaScript
1const original ={title:'Dune',onLoan:false}2const same = original3same.onLoan =true4console.log(original.onLoan)// true: one object, two names56const copy ={...original,onLoan:false}7console.log(original.onLoan)// still true: copy is separate8const titles =['Emma']9const more =[...titles,'Dune']// ['Emma', 'Dune']
Predict the outputInteractive · disabled in the PDF
What does this print?
JavaScript
1const a =['Emma']2const b = a3b.push('Dune')4console.log(a.length)
Make sameShelf a copy with [...shelf], or copy the same object with = book, and run it again.
TypeScript46 / 189
Module 5 · Arrays, objects and missing values
Recap
Arrays are ordered lists, indexed from 0; for...of visits each element.
map transforms, filter keeps matches, find returns the first match or undefined.
Objects group named properties, read with .key or [key].
undefined means never set, null means empty on purpose. Reading a property of either crashes.
?. reads safely; ?? gives a fallback only for null and undefined.
Objects and arrays are shared by reference; spread ... makes a copy.
FlashcardsInteractive · disabled in the PDF
Index
An element’s position in an array, from 0.
filter vs find
filter returns every match in a new array; find the first match or undefined.
Property
A named value in an object: book.title.
undefined
No value was ever set.
null
Deliberately empty.
a ?? b
a, unless it is null or undefined; then b.
Reference
What a variable holds for an object: a pointer, so copies of it share the object.
Spread ...
Copies the properties or elements into a new object or array.
Module 6: Modules, errors and waiting
TypeScript47 / 189
Module 6 · Modules, errors and waiting
A module is a file that shares some of its names
Real programs are split across many files. Each file is a module: its variables and functions are private to it, unless it exports them. Another file can then import them by name. This keeps each file small and focused.
library/
package.json# the project's settings
fees.js# exports lateFee
main.js# imports and uses it
fees.js
exportfunctionlateFee(days){return days *2}const secretRate =3// not exported
Use a name another module exports. ./ means "in this folder".
TypeScript48 / 189
Module 6 · Modules, errors and waiting
Packages are modules other people wrote
A package is a bundle of modules published for anyone to use. npm is the tool (installed with Node) that downloads them into a node_modules folder and records them in package.json. You import a package by its name, without ./.
Terminal
1npminit-y# create package.json in this folder2npminstalldate-fns# download a package for working with dates
JavaScript
1import{ addDays }from'date-fns'23const due =addDays(newDate(),14)// two weeks from now
new Date() is the current date and time, built into JavaScript.
Two kinds of module in Node. Node runs import/export (ES modules) when package.json has "type": "module", or the file ends in .mjs. Older code uses require(...) and module.exports (CommonJS). This course uses ES modules, the standard.
QuizInteractive · disabled in the PDF
Which import loads a file called members.js sitting next to the current file?
Which import loads a file called members.js sitting next to the current file?
TypeScript49 / 189
Module 6 · Modules, errors and waiting
throw signals a problem, catch handles it
When a function cannot do its job, it can throw an error. Throwing stops the function at once, and the error travels back up through every caller until something catches it with try...catch. If nothing does, the program stops with the error you saw in module 1.
JavaScript
1functionborrow(book){2if(book.onLoan){3thrownewError(`${book.title} is already on loan`)4}5 book.onLoan =true6}78try{9borrow({title:'Dune',onLoan:true})10 console.log('Enjoy your book')// skipped11}catch(error){12 console.log(error.message)// Dune is already on loan13}
QuizInteractive · disabled in the PDF
In the code above, why is "Enjoy your book" never printed?
In the code above, why is "Enjoy your book" never printed?
PlaygroundInteractive · disabled in the PDF
JavaScript
1functionborrow(book){2if(book.onLoan){3thrownewError(`${book.title} is already on loan`)4}5 book.onLoan =true6}78try{9borrow({title:'Dune',onLoan:true})10 console.log('Enjoy your book')11}catch(error){12 console.log(error.message)13}14console.log('The library stays open')
Set onLoan to false and run it. Then remove the try...catch around the call and see what stops.
TypeScript50 / 189
Module 6 · Modules, errors and waiting
Some work finishes later
Some jobs take time: downloading data, reading a file, waiting for a timer. JavaScript does not stop and wait for them. It starts the job, carries on with the next lines, and runs your code for the result later. Work like this is asynchronous; ordinary line-by-line work is synchronous.
Predict the outputInteractive · disabled in the PDF
Runs now, line by line; the next line waits for it.
Asynchronous
Started now, finished later; the next line does not wait.
TypeScript51 / 189
Module 6 · Modules, errors and waiting
A promise is a value that arrives later
Asynchronous functions return a promise: an object standing in for a result that is not ready yet. A promise starts pending, then either fulfils with a value or rejects with an error. It settles once and never changes after that.
The three states of a promise.
Promise
An object for a result that will be ready later.
Fulfil
Finish successfully, with a value.
Reject
Finish with an error.
TypeScript52 / 189
Module 6 · Modules, errors and waiting
await waits for a promise inside an async function
Inside a function marked async, await pauses that function until a promise settles, then gives you its value, so asynchronous code reads like ordinary code. fetch is the built-in way to ask a server for data; it returns a promise. Data often comes back as JSON, a text format that looks like JavaScript objects and arrays.
Lines 1:async lets the function use await. It now always returns a promise.
Lines 2: Ask the server for data and wait for its response.
Lines 3: Reading the body as JSON takes time too, so it is awaited as well.
Lines 7: Outside an async function, .then(callback) runs the callback with the value once the promise fulfils.
async function
A function that can await, and that returns a promise.
await
Pause until a promise settles, then use its value.
JSON
A text format for data: {"title": "Dune", "pages": 412}. Keys are always in double quotes.
API
A set of addresses a server answers with data, for programs rather than people.
TypeScript53 / 189
Module 6 · Modules, errors and waiting
Awaited errors are caught with try...catch
When an awaited promise rejects, await throws its error, so ordinary try...catch handles it. The usual mistake goes the other way: forgetting await. Then you get the promise itself, not its value, and the code after it runs too early.
JavaScript
1asyncfunctionloadMember(id){2try{3const url =`https://example.com/api/members/${id}`4const response =awaitfetch(url)5returnawait response.json()6}catch(error){7 console.log('Could not load the member:', error.message)8returnnull9}10}
Find the bugInteractive · disabled in the PDF
This prints Promise { <pending> } instead of the books. Which line is wrong?
JavaScript
TypeScript54 / 189
Module 6 · Modules, errors and waiting
Try it: count, or fall back
countBooks is given load, an async function that gives an array of titles, or rejects when the library’s server is down. The tests pass in pretend versions of load, so nothing really goes over the network.
Code challengeInteractive · disabled in the PDF
Finish countBooks(load): await load() and return how many titles it gave. If it rejects, catch the error and return 0.
JavaScript
1exportasyncfunctioncountBooks(load){2// TODO: await load(), and return the number of titles.3// If it rejects, return 0.4return-15}
TypeScript55 / 189
Module 6 · Modules, errors and waiting
Promise.all waits for several at once
Awaiting one thing after another is slow when the jobs do not depend on each other. Promise.all takes an array of promises and gives one promise that fulfils with all their values, in the same order, once every one has fulfilled. If any of them rejects, the whole thing rejects.
One after the other
const books =awaitloadBooks()const members =awaitloadMembers()
Note.const [books, members] = ... is destructuring: it takes the first and second elements of the array and names them. Objects can be destructured too: const { title, pages } = book.
QuizInteractive · disabled in the PDF
Loading books takes 2 seconds and loading members takes 3. About how long does the Promise.all version take?
Loading books takes 2 seconds and loading members takes 3. About how long does the Promise.all version take?
TypeScript56 / 189
Module 6 · Modules, errors and waiting
Recap
Each file is a module; export shares a name and import uses it.
Packages are installed with npm and imported by name.
throw stops and signals an error; try...catch handles it.
Asynchronous work finishes later; its result comes as a promise.
In an async function, await waits for a promise and throws if it rejects. Do not forget it.
Promise.all runs independent jobs at the same time.
FlashcardsInteractive · disabled in the PDF
Module
A file with its own names, sharing some by export.
Package
Published modules, installed with npm install.
throw
Stops the function and sends an error up to whoever catches it.
Asynchronous
Work that starts now and finishes later.
Promise
A stand-in for a result: pending, then fulfilled or rejected.
await
Inside async, pauses until a promise settles and gives its value.
JSON
A text format for data that looks like JavaScript objects.
Destructuring
const { title } = book or const [a, b] = list: take parts out by name or position.
Module 7: Meet TypeScript
TypeScript57 / 189
Module 7 · Meet TypeScript
JavaScript finds mistakes only when it runs
Look back at the bugs in the first modules: '2' + 2 giving '22', arguments in the wrong order, reading .name of undefined, a forgotten await. JavaScript ran every one of them without a word, and you found out only when the program did the wrong thing, maybe in front of a user.
TypeScript is JavaScript plus types: labels that say what kind of value each variable, parameter and property holds. A program called the type checker reads those labels and reports code that cannot be right, before anything runs.
Type
A description of a set of values: number is every number, string every text.
Type checker
The program that checks your code against its types. For TypeScript, tsc.
Compile time
When the checker reads your code, before it runs.
Run time
When the code is actually running. Also written "runtime" for the program that runs it.
TypeScript58 / 189
Module 7 · Meet TypeScript
An annotation says what a value must be
A type annotation is a colon and a type after a name. TypeScript files end in .ts instead of .js. Everything else you learned about JavaScript still holds: TypeScript adds to the language, it does not replace it.
Lines 1:daysLate: number: this parameter must be a number.
Lines 1: The : number after the brackets is the return type: what the function gives back.
Lines 5: A number goes in: the checker is satisfied.
Lines 6: A string goes in. The checker reports it at once, with the line and the reason. In plain JavaScript this would quietly give 8 (because '4' * 2 converts), or worse.
Run it. Then change 4 to '4': the error appears before anything runs. Put it back and it goes away.
TypeScript59 / 189
Module 7 · Meet TypeScript
Reading a type error
A type error has a location, a code (TS and a number, useful for searching), and a sentence. Most sentences follow one pattern: "Type A is not assignable to type B", meaning "you gave me an A where a B is required". Read it as: what did I give, and what was wanted?
Text
1fees.ts:6:22 - error TS2345: Argument of type 'string' is not2assignable to parameter of type 'number'.346 const oops = lateFee('4')5 ~~~
File, line 6, column 22; the code; what was given (string) and what was wanted (number).
QuizInteractive · disabled in the PDF
The checker says: "Type 'number' is not assignable to type 'string'." Which line could cause it?
The checker says: "Type 'number' is not assignable to type 'string'." Which line could cause it?
TypeScript60 / 189
Module 7 · Meet TypeScript
Types are removed before the code runs
Browsers and Node run JavaScript, not TypeScript. So before running, the annotations are erased: deleted, leaving ordinary JavaScript. Checking and erasing are two separate jobs, often done by different tools.
The checker only reports. The JavaScript that runs is your code minus the types.
You write
const pages:number=412functionhalf(n:number):number{return n /2}
What runs
const pages =412functionhalf(n){return n /2}
The one fact to remember. Types describe your program; they are not part of it. A type error never changes what the code does, and at run time nothing checks that a value really matches its type. Data that comes from outside (a server, a file, a form) must be checked by your own code.
TypeScript61 / 189
Module 7 · Meet TypeScript
Installing and running the checker
The playground checks as you type, so you can skip this until you start a real project. There, TypeScript is a package installed per project. Its command is tsc, run with npx (which runs a command from an installed package). Its settings live in tsconfig.json.
Terminal
1npminstall--save-devtypescript# add it to the project2npxtsc--init# write a tsconfig.json with good defaults3npxtsc--noEmit# check every file, write nothing4npxtsc# check, and write .js files5nodefees.ts# recent Node runs .ts directly, unchecked
--save-dev marks it as a tool for development, not part of the finished app.
Node can strip types. Node 22.18 and later (and 23.6 and later) can run a .ts file by erasing its types, without checking them. It is handy for scripts. It only handles syntax that can simply be deleted, which is one reason modern code avoids the few TypeScript features that generate code (covered in the expert modules).
Try itInteractive · disabled in the PDF
TypeScript62 / 189
Module 7 · Meet TypeScript
TypeScript works most types out itself
You do not have to annotate everything. TypeScript infers a type from the value: const pages = 412 is a number without saying so. In an editor such as VS Code, hold the mouse over a name to see the type it inferred. Annotate where values come in: function parameters, always.
TypeScript
1let title ='Dune'// inferred: string2let copies =3// inferred: number3title =42// error: number is not assignable to string45const fee =lateFee(4)// inferred from lateFee's return type6const titles =['Emma','Dune']// inferred: string[]
Predict the outputInteractive · disabled in the PDF
What type does TypeScript infer for total?
TypeScript
1functiontotal(prices:number[]){2let sum =03for(const p of prices) sum += p4return sum5}
What type does TypeScript infer for total?
TypeScript63 / 189
Module 7 · Meet TypeScript
Try it: annotate, then fix what the checker finds
This exercise starts with an error on purpose. In strict mode a parameter with no type is an error ("implicitly has an 'any' type"). Fix it, and watch the checker find the next problem.
Code challengeInteractive · disabled in the PDF
Give lateFee a number parameter and a number return type. Then fix the call the checker now rejects, so fee is 8.
Editors with TypeScript support, such as VS Code, run the checker constantly. Errors appear as red wavy underlines; hovering shows the message. As you type a dot, the editor lists what is available, from the types: this is autocomplete, and it is half the reason people use TypeScript.
Find the bugInteractive · disabled in the PDF
The editor underlines one line in red. Which one?
TypeScript
TypeScript65 / 189
Module 7 · Meet TypeScript
Always turn strict mode on
TypeScript can be more or less demanding. "strict": true in tsconfig.json turns on a family of stricter checks, the most important being strict null checks: null and undefined are not allowed where a real value is expected. tsc --init turns it on, and every example in this course assumes it.
tsconfig.json
1{2"compilerOptions":{3"strict":true4}5}
A real tsconfig has more settings; the expert modules go through them.
Predict the outputInteractive · disabled in the PDF
With strict on, what does the checker say about this?
TypeScript
1const members =[{id:1,name:'Ada'}]2const member = members.find((m)=> m.id ===2)3console.log(member.name)
With strict on, what does the checker say about this?
TypeScript66 / 189
Module 7 · Meet TypeScript
Recap
TypeScript is JavaScript plus types; the checker reports impossible code before it runs.
Annotate with : type; parameters always, the rest is mostly inferred.
"A is not assignable to B" means you gave an A where a B was needed.
Types are erased before running. Nothing checks them at run time.
npx tsc --noEmit checks a project; the editor checks as you type.
Keep strict on. It is what makes undefined a type error.
FlashcardsInteractive · disabled in the PDF
Type
A description of a set of values, like number.
Type annotation
: type after a name, saying what it must hold.
Inference
TypeScript working out a type from the code itself.
Erasure
Removing the types to get the JavaScript that runs.
tsc
The TypeScript compiler and checker.
tsconfig.json
A project’s TypeScript settings.
Strict null checks
null and undefined are only allowed where the type says so.
Module 8: Everyday types
TypeScript67 / 189
Module 8 · Everyday types
The basic types match the basic values
Each kind of value from the JavaScript modules has a type with the same name, in lowercase: string, number, boolean, null and undefined. These are the primitive types, the building blocks every other type is made from.
TypeScript
1const title:string='Dune'2const pages:number=412// whole numbers and decimals alike3const onLoan:boolean=false4const borrower:null=null
Annotated here to show the names. In real code you would let these be inferred.
Watch out. Write string, not String. The capitalised names are JavaScript objects that wrap primitives, and using them as types is almost always a mistake.
Primitive
A simple value: a string, number, boolean, null or undefined (and two rarer ones, bigint and symbol).
TypeScript68 / 189
Module 8 · Everyday types
Array types: string[] and friends
An array of strings is string[]: the element type followed by []. Array<string> means the same; you will see both. Every element must match, and everything you get out of the array has that type.
TypeScript
1const titles:string[]=['Emma','Dune']2titles.push('Beloved')// fine3titles.push(42)// error: number is not assignable to string45const loud = titles.map((t)=> t.toUpperCase())// string[]6const sizes = titles.map((t)=> t.length)// number[]
Predict the outputInteractive · disabled in the PDF
What is the type of prices?
TypeScript
1const prices =[4.5,12,0]
What is the type of prices?
TypeScript69 / 189
Module 8 · Everyday types
An object type lists the properties
An object type looks like an object with types instead of values: each property name, a colon, its type. An object must have every property listed, with the right types. Property lines can be separated by ;, , or a new line.
TypeScript
1functiondescribe(book:{ title:string; pages:number}){2return`${book.title}, ${book.pages} pages`3}45describe({title:'Dune',pages:412})// fine6describe({title:'Dune'})7// error: Property 'pages' is missing in type '{ title: string; }'
Delete pages: 412 and read the error. Then try pages: '412', and book.author inside the function.
TypeScript70 / 189
Module 8 · Everyday types
Name a type with type or interface
Writing the same object type over and over is tiring and easy to get wrong. A type alias gives any type a name with type Name = .... An interface names an object type with interface Name { ... }. Type names start with a capital letter, by convention.
Names any other type (a union, a tuple, a primitive)
Yes
No
Can be extended
With &
With extends
Two with the same name
An error
Merged into one
Tip. Either is fine for objects; pick one style per project. Many teams use interface for object shapes and type for everything else. The differences matter in rare cases, covered later.
TypeScript71 / 189
Module 8 · Everyday types
Optional and readonly properties
A ? after a property name makes it optional: it may be missing. Reading it then gives the type orundefined, and the checker makes you handle both. readonly before a name means it may be read but not reassigned.
TypeScript
1interfaceBook{2readonly isbn:string3 title:string4 subtitle?:string// string | undefined when read5}67functionheading(book:Book){8 book.isbn ='x'// error: isbn is read-only9return book.subtitle?.toUpperCase()?? book.title10}
QuizInteractive · disabled in the PDF
In heading, why does book.subtitle.toUpperCase() (without ?.) fail to compile?
In heading, why does book.subtitle.toUpperCase() (without ?.) fail to compile?
TypeScript72 / 189
Module 8 · Everyday types
Typing functions and callbacks
Parameters always get a type; the return type is inferred unless you want to fix it. A parameter can be optional (days?: number) or have a default (days = 14, inferred as number). A function that returns nothing has the return type void. To describe a function as a value, such as a callback, write (params) => result.
The callback’s book needs no annotation: it is known from BookTest. This is called contextual typing.
Find the bugInteractive · disabled in the PDF
One line fails to compile. Which?
TypeScript
TypeScript73 / 189
Module 8 · Everyday types
Saying a value may be missing
With strict mode, null and undefined are only allowed where the type says so. The | symbol means "or": Member | null is a member or null. Before using such a value, you must check it, and the checker notices the check.
TypeScript
1functionfindMember(id:number):Member|undefined{2return members.find((m)=> m.id === id)3}45const member =findMember(2)6console.log(member.name)// error: possibly 'undefined'7if(member){8 console.log(member.name)// fine: here it is a Member9}
QuizInteractive · disabled in the PDF
Which declaration compiles with strict on?
Which declaration compiles with strict on?
TypeScript74 / 189
Module 8 · Everyday types
Try it: a label for a book
An interface with an optional property, and a function that has to handle it being missing.
Code challengeInteractive · disabled in the PDF
Finish label(book): 'Dune by Frank Herbert', or with a subtitle, 'Dune: Desert Planet by Frank Herbert'.
A fixed-length array with a type per position: [string, number].
Type alias
type Name = ...: a name for any type.
Interface
interface Name { ... }: a name for an object type. Same-named ones merge.
Optional property
subtitle?: string: may be missing; reads as string | undefined.
void
The return type of a function that returns nothing useful.
Contextual typing
A callback’s parameters typed from where it is passed.
Module 9: Unions and narrowing
TypeScript76 / 189
Module 9 · Unions and narrowing
A literal type is one exact value
A type can be a single value: the type 'available' allows only that string. On its own that is not useful, but combined with | it describes a fixed set of choices. TypeScript infers literal types for const, because a const can never change. A let is widened to the general type, since it may be reassigned.
TypeScript
1const status ='available'// type: 'available'2let mood ='happy'// type: string (widened)34typeStatus='available'|'on-loan'|'lost'5let current:Status='available'6current ='on-loan'// fine7current ='missing'// error: not one of the three
Literal type
A type with exactly one value: 'lost', 42, true.
Widening
TypeScript generalising 'happy' to string where a value could change.
TypeScript77 / 189
Module 9 · Unions and narrowing
A union is one of several types
A union typeA | B holds a value of type Aor type B. A union of literals is a list of choices. A union of different types, such as string | number, is something you have to check before using, because you can only do what every member allows.
Predict the outputInteractive · disabled in the PDF
TypeScript follows your if statements. Inside a branch that checked typeof id === 'string', it knows id is a string; in the else, what is left: number. This is narrowing: the checker reading your checks and shrinking the type to match.
WalkthroughInteractive · disabled in the PDF
TypeScript
1functionlabel(id:string|number){2if(typeof id ==='string'){3return id.toUpperCase()4}5return`#${id.toFixed(0)}`6}
Lines 1: Here id is string | number.
Lines 2-3: Inside the if, id is narrowed to string, so toUpperCase is allowed.
Lines 5: The if returned, so if we get here id was not a string: it is number, and toFixed (a number method) is allowed.
Narrowing
The checker shrinking a type after a check, inside the code the check guards.
Type guard
A check that narrows: typeof x === 'string', x !== null, 'key' in x.
TypeScript79 / 189
Module 9 · Unions and narrowing
Every everyday check narrows
Check
Narrows
Example
typeof
Primitives, and functions
typeof x === 'number'
Truthiness
Removes null, undefined (and falsy values)
if (member) { ... }
Equality
To the value compared with
if (status === 'lost')
in
Objects with that property
if ('dueDate' in loan)
instanceof
Objects made by a class
if (e instanceof Error)
Array.isArray
Arrays
if (Array.isArray(x))
Predict the outputInteractive · disabled in the PDF
What is wrong with this function?
TypeScript
1functionfineText(fine:number|undefined){2if(fine){3return`You owe ${fine}`4}5return'No fine recorded'6}
What is wrong with this function?
TypeScript80 / 189
Module 9 · Unions and narrowing
Discriminated unions: a tag says which shape
A loan is either active (it has a due date) or returned (it has a return date). Rather than one type with many optional properties, use a union of object types that share a tag property with a different literal in each. Checking the tag narrows the whole object. This is a discriminated union, and it is the most useful pattern in this course.
On the last line of describe, what is the type of loan?
On the last line of describe, what is the type of loan?
TypeScript81 / 189
Module 9 · Unions and narrowing
Make the checker prove every case is handled
With a switch over the tag, each case narrows. After every case is handled, nothing is left, and TypeScript calls that empty type never. Assigning the value to a never variable in default turns "I forgot a case" into a compile error, the day someone adds a fourth status.
A function can be a type guard too. Its return type value is Type is a type predicate: it tells the checker "if I return true, the argument is this type". Since TypeScript 5.5, simple guards like the one in filter below are inferred without writing it.
TypeScript
1typeActiveLoan=Extract<Loan,{ status:'active'}>23functionisActive(loan:Loan): loan isActiveLoan{4return loan.status ==='active'5}67const active = loans.filter(isActive)// only active loans8const named = names.filter((n)=> n !==null)// string[], since 5.5
Extract<Loan, ...> picks the union members that match a shape. Utility types like it come in the advanced modules.
Fill the blanksInteractive · disabled in the PDF
Complete a guard that tells the checker a value is a string.
TypeScript
1functionisText(value:unknown): value string{2return value ==='string'3}
TypeScript84 / 189
Module 9 · Unions and narrowing
Recap
Literal types are single values; const keeps them, let widens.
A union allows any member, but you can only use what every member has.
typeof, truthiness, ===, in and instanceof checks narrow a union.
Discriminated unions share a tag property; checking the tag narrows the object.
A never check in default makes forgetting a case a compile error.
A value is T return type turns a function into a type guard.
FlashcardsInteractive · disabled in the PDF
Union type
A | B: a value of either type.
Narrowing
The checker shrinking a type inside code guarded by a check.
Discriminated union
A union of object types told apart by a shared literal tag.
never
The type with no values: what remains when every case is handled.
Exhaustiveness check
const x: never = value in default, to catch missing cases.
Type predicate
value is T as a return type: "true means it is a T".
Module 10: Shapes, classes and escape hatches
TypeScript85 / 189
Module 10 · Shapes, classes and escape hatches
Types are compared by shape, not by name
TypeScript does not care what a type is called. A value fits a type if it has the properties the type asks for, with the right types. This is structural typing: "if it has a title and an author, it is a Book". A value with more properties than needed fits too.
TypeScript
1interfaceTitled{ title:string}2interfaceBook{ title:string; author:string; pages:number}34functionlabel(item:Titled){5return item.title6}78const dune:Book={title:'Dune',author:'Herbert',pages:412}9label(dune)// fine: a Book has a title, so it is Titled
Structural typing
Types fit when their shapes fit, whatever they are named.
Assignable
Allowed where another type is expected. "A is assignable to B" means an A can be used as a B.
Note. Many languages (Java, C#) use names instead: a class must say it implements an interface. TypeScript describes JavaScript, where objects are just bags of properties, so shape is what matters.
TypeScript86 / 189
Module 10 · Shapes, classes and escape hatches
Fresh object literals are checked for extras
There is one exception to "more is fine". When you write an object literal directly where a type is expected, an unknown property is an error. It is usually a typo, and nothing else could ever read it. This is the excess property check.
Predict the outputInteractive · disabled in the PDF
Which line is an error?
TypeScript
1interfaceMember{ id:number; name:string}23const a:Member={id:1,name:'Ada',nmae:'x'}// A4const raw ={id:1,name:'Ada',joined:2024}5const b:Member= raw // B
Which line is an error?
TypeScript87 / 189
Module 10 · Shapes, classes and escape hatches
Classes make objects of the same shape
A class is a JavaScript template for making objects. new calls its constructor, which sets up the object's fields. TypeScript lets you declare the fields with types, and mark members private (only usable inside the class). JavaScript's own #name fields are private at run time too.
An intersectionA & B is a value that is both an A and a B: it has all the properties of each. It is how you add properties to an existing type alias. Interfaces do the same with extends.
any means "do not check this". Anything can be assigned to it, it can be assigned to anything, and every property and call on it is allowed. It also spreads: whatever you get from an any is any. Some built-ins return it, most famously JSON.parse.
TypeScript
1const data:any= JSON.parse('{"title": "Dune"}')2data.title.toUpperCase()// allowed3data.pagse.toFixed()// allowed, and crashes at run time4const pages:number= data.x // allowed: any fits everywhere
Watch out. Treat any as a hole in your types. Strict mode reports parameters that would silently become any ("implicitly has an 'any' type"). If you truly do not know a type, use unknown, on the next slide.
TypeScript91 / 189
Module 10 · Shapes, classes and escape hatches
unknown: anything goes in, nothing comes out unchecked
unknown also accepts any value, but you can do nothing with it until you narrow it. It is the honest type for data from outside: a parsed file, a server response, user input. The checks you write to narrow it are exactly the checks that keep the program from crashing.
TypeScript
1const data:unknown= JSON.parse(text)2data.title // error: 'data' is of type 'unknown'34if(5typeof data ==='object'&& data !==null&&6'title'in data &&typeof data.title ==='string'7){8 console.log(data.title.toUpperCase())// fine9}
typeof null is 'object' in JavaScript, which is why data !== null is needed too.
any
unknown
never
Accepts
Anything
Anything
Nothing
Can be used as
Anything, unchecked
Nothing, until narrowed
Anything (it never exists)
Use it for
Almost never
Values you have not checked yet
Impossible cases
TypeScript92 / 189
Module 10 · Shapes, classes and escape hatches
as tells the checker to trust you
A type assertion, value as Type, tells the checker to treat a value as a type, without any check at run time. The ! after an expression is the same for null and undefined: "trust me, it is there". Both are promises you make; if you are wrong, the crash happens later, far from the cause.
A promise nobody checks
const book = JSON.parse(text)asBookconst member =findMember(2)!member.name
A check the checker can see
const member =findMember(2)if(!member)thrownewError('No member 2')member.name
Find the bugInteractive · disabled in the PDF
Everything compiles, but one line makes a later crash possible. Which?
TypeScript
TypeScript93 / 189
Module 10 · Shapes, classes and escape hatches
Recap
Types fit by shape. Extra properties are fine, except in a fresh object literal.
Classes make objects with typed fields; private and readonly restrict them.
A & B has everything from both; conflicting primitives give never.
any switches checking off and spreads. Avoid it.
unknown is the safe type for unchecked data: narrow before use.
as and ! are unchecked promises. Prefer a check the checker can follow.
FlashcardsInteractive · disabled in the PDF
Structural typing
Compatibility by shape, not by name.
Excess property check
An error for unknown properties in an object literal written directly into a type.
Intersection
A & B: has the properties of both.
any
No checking, in both directions. It spreads.
unknown
Accepts anything; usable only after narrowing.
Type assertion
x as T: tells the checker to trust you. Not checked at run time.
Non-null assertion
x!: claims x is not null or undefined.
Module 11: Typing async code, data and errors
TypeScript94 / 189
Module 11 · Typing async code, data and errors
An async function returns a Promise<T>
The type of a promise says what it will fulfil with: Promise<number> is a promise of a number. The angle brackets hold a type argument, a type passed to another type, like an argument to a function. An async function always returns a promise, so its return type is Promise<...>, and await unwraps it.
TypeScript
1asyncfunctioncountBooks():Promise<number>{2const books =awaitloadBooks()// books: Book[]3return books.length // a number, wrapped for you4}56const n =countBooks()// Promise<number>7const m =awaitcountBooks()// number
await at the top level of a module works in ES modules.
Predict the outputInteractive · disabled in the PDF
response.json() returns Promise<any>: TypeScript cannot know what a server will send. Annotating the result (const books: Book[] = await response.json()) compiles, but it is a promise nobody checks. The boundary where data enters your program is where you must check it at run time.
Inside the boundary, types can be trusted. At the boundary, check.
Tip. Treat the result of response.json() and JSON.parse as unknown. That forces you, and everyone after you, to check it before use.
TypeScript96 / 189
Module 11 · Typing async code, data and errors
A validating function earns the type
Write a type guard that checks the shape, and the type is backed by a real check. For larger shapes, a validation library such as Zod lets you describe the shape once and get both the check and the type from it; it has its own course.
TypeScript
1functionisBook(value:unknown): value isBook{2return(3typeof value ==='object'&& value !==null&&4'title'in value &&typeof value.title ==='string'&&5'author'in value &&typeof value.author ==='string'&&6'pages'in value &&typeof value.pages ==='number'7)8}910asyncfunctionloadBooks():Promise<Book[]>{11const response =awaitfetch('https://example.com/api/books')12const data:unknown=await response.json()13if(!Array.isArray(data)||!data.every(isBook)){14thrownewError('The server sent something that is not books')15}16return data17}
QuizInteractive · disabled in the PDF
Why annotate data as unknown rather than leaving it as any?
Why annotate data as unknown rather than leaving it as any?
TypeScript97 / 189
Module 11 · Typing async code, data and errors
fetch does not fail on an error status
Every answer from a server has a status code: 200 means OK, 404 not found, 500 a server error. fetch only rejects when there is no answer at all (no network, say). A 404 fulfils normally, so you must check response.ok (true for 200 to 299) yourself. Types do not help here: both are a Response.
Find the bugInteractive · disabled in the PDF
When the book does not exist, the server answers 404 with {"error": "Not found"}. Which line should have been there to stop this from being treated as a book?
TypeScript
TypeScript98 / 189
Module 11 · Typing async code, data and errors
A caught error is unknown
JavaScript can throw any value, not only Error objects. So in strict mode the variable in catch (error) has the type unknown, and you narrow it before reading .message. instanceof Error is the usual check.
TypeScript
1try{2awaitloadBooks()3}catch(error){4// error: unknown5const message = error instanceofError? error.message :String(error)6 console.log(`Could not load the catalogue: ${message}`)7}
Fill the blanksInteractive · disabled in the PDF
Narrow the caught value before reading its message.
Return failures as values when callers must handle them
A thrown error is invisible in a function’s type: nothing in borrow(book): void says it might throw. For failures callers are expected to handle, return them instead, as a discriminated union. Now the type shows every outcome, and narrowing forces the caller to look.
TypeScript
1typeResult<T>=2|{ ok:true; value:T}3|{ ok:false; error:string}45functionborrow(book:Book, member:Member):Result<Loan>{6if(member.fines >0)return{ok:false,error:'Unpaid fines'}7if(book.onLoan)return{ok:false,error:'Already on loan'}8return{ok:true,value:createLoan(book, member)}9}1011const result =borrow(dune, ada)12if(result.ok) console.log(result.value.bookId)13else console.log(result.error)
Result<T> takes a type argument, like Promise<T>. The next module shows how to write types like it.
QuizInteractive · disabled in the PDF
Right after const result = borrow(dune, ada), why is result.value an error?
Right after const result = borrow(dune, ada), why is result.value an error?
TypeScript100 / 189
Module 11 · Typing async code, data and errors
Try it: parse without throwing
Text typed into a form is a common place for a Result: bad input is expected, not a surprise. Number(text) turns text into a number, and gives NaN ("not a number") when it cannot.
Code challengeInteractive · disabled in the PDF
Finish parsePages(text): a success with the number for text like '310'; a failure with the error 'Not a number' when Number(text) is NaN, or 'Must be more than 0' when it is 0 or less.
Promise.all on promises of different types gives a tuple with each result’s own type, in order. The built-in Awaited<T> type gives what a promise fulfils with, even nested ones: Awaited<Promise<number>> is number.
ReturnType and typeof in a type come in the advanced modules.
TypeScript102 / 189
Module 11 · Typing async code, data and errors
Recap
async functions return Promise<T>; await unwraps it.
The checker catches using a promise as its value; a linter catches ignoring one.
Data from outside is unknown until a run-time check proves its shape.
Check response.ok: fetch fulfils on 404 and 500.
Caught errors are unknown; narrow with instanceof Error.
Return a Result union for failures callers must handle; throw for surprises.
FlashcardsInteractive · disabled in the PDF
Promise<T>
A promise that fulfils with a T.
Type argument
A type passed to a type: the number in Promise<number>.
Boundary
Where data enters your program. Check it there.
response.ok
True for status 200 to 299. fetch does not reject on errors.
Type of a caught error
unknown, because anything can be thrown.
Result type
A union of success and failure, returned instead of thrown.
Awaited<T>
What a promise type fulfils with, unwrapping nested promises.
Module 12: Generics
TypeScript103 / 189
Module 12 · Generics
Some code works the same for every type
A function that returns the first element of a list works for books, members and numbers alike. Typing it with unknown[] loses the type: you get unknown back. Typing it with any[] loses checking. What you want is to say "whatever type of list goes in, that type of element comes out".
Loses the type
functionfirst(list:unknown[]){return list[0]}const t =first(['Emma'])// unknown
Keeps it
functionfirst<T>(list:T[]){return list[0]}const t =first(['Emma'])// string
Generic
Code with a type parameter, so it works for many types.
Type parameter
A name for a type, written in angle brackets: the T in first<T>.
TypeScript104 / 189
Module 12 · Generics
A type parameter is a variable for a type
<T> after the function name declares a type parameter. Like a normal parameter, it is filled in at each call, but with a type. Usually you do not pass it: TypeScript infers it from the arguments. T is a convention for "type"; descriptive names like Item are fine too.
WalkthroughInteractive · disabled in the PDF
TypeScript
1functionfirst<T>(list:T[]):T|undefined{2return list[0]3}45const title =first(['Emma','Dune'])// string | undefined6const pages =first([310,412])// number | undefined7const none =first<Book>([])// Book | undefined
Lines 1: Declare T, take an array of T, return a T (or undefined for an empty list).
Lines 5: The argument is a string[], so T is inferred as string.
Lines 6: Same function, new call: T is number.
Lines 7: An empty array gives nothing to infer from, so pass the type argument yourself.
Predict the outputInteractive · disabled in the PDF
Inside a generic function, T could be anything, so you cannot read properties of it. A constraint, T extends Shape, limits T to types that fit the shape. You can then use what the shape guarantees, and callers still get their exact type back.
TypeScript
1functionlongest<Textends{ length:number}>(a:T, b:T):T{2return a.length >= b.length ? a : b3}45longest('Emma','Beloved')// 'Emma' | 'Beloved'6longest([1,2],[3])// number[]7longest(10,20)// error: number has no 'length'
condition ? a : b is the conditional operator: a if the condition is true, otherwise b.
Find the bugInteractive · disabled in the PDF
Line 2 fails to compile. Which line has to change to fix it?
TypeScript
TypeScript106 / 189
Module 12 · Generics
K extends keyof T: a key of the object
keyof T is the union of T’s property names: keyof Book is 'title' | 'author' | 'pages'. T[K] is the type of property K in T. Together they type functions that work with any property, and still know each property’s own type.
TypeScript
1functionpluck<T,KextendskeyofT>(items:T[], key:K):T[K][]{2return items.map((item)=> item[key])3}45pluck(catalogue,'title')// string[]6pluck(catalogue,'pages')// number[]7pluck(catalogue,'colour')// error: not a key of Book
Fill the blanksInteractive · disabled in the PDF
Type a function that reads one property of an object.
A Map is a lookup table: map.set(key, value) stores, map.get(key) finds (or gives undefined), and map.size counts. indexBy builds one from a list, keyed by one property of each item. The tests include a type test: it compiles only when the type is exactly right.
Code challengeInteractive · disabled in the PDF
Finish indexBy(items, key): give it a second type parameter K for the key, return a Map<T[K], T>, and fill it with each item under its key property.
You have used generic types already: Promise<T>, Array<T> and the Result<T> from the last module. Any type or interface can take type parameters, with constraints and defaults (= Type, used when none is given).
Classes take type parameters after their name. Every instance fixes the parameter once, and all its methods agree on it. This is how the built-in Map<K, V> and Set<T> are typed.
TypeScript
1classQueue<T>{2private items:T[]=[]3add(item:T){4this.items.push(item)5}6next():T|undefined{7returnthis.items.shift()8}9}1011const holds =newQueue<Member>()12holds.add(ada)13holds.add('Ada')// error: string is not a Member1415const byIsbn =newMap<string,Book>()16byIsbn.get('x')// Book | undefined
shift removes and returns the first element.
TypeScript110 / 189
Module 12 · Generics
A good type parameter links two places
A type parameter earns its place when it connects things: an input to an output, two inputs to each other. One that appears only once does nothing a plain type could not. One that appears only in the return type is worse: it is an unchecked as in disguise, because the caller picks a type and nothing checks it.
QuizInteractive · disabled in the PDF
Which signature is really a hidden type assertion?
Which signature is really a hidden type assertion?
Expert note. Rule of thumb from the TypeScript team: if a type parameter appears only once in a signature, you probably do not need it. Prefer the simplest signature that keeps the information callers need.
TypeScript111 / 189
Module 12 · Generics
Recap
A type parameter <T> is a variable for a type, usually inferred from the arguments.
T extends Shape constrains it, so you can use what the shape guarantees.
K extends keyof T and T[K] type functions over any property.
Types, interfaces and classes can be generic, with defaults.
A useful type parameter links two places; one only in the return type is a disguised cast.
FlashcardsInteractive · disabled in the PDF
Type parameter
<T>: a name for a type, filled in per use.
Constraint
T extends X: T must be assignable to X.
keyof T
The union of T’s property names.
T[K]
The type of property K of T: an indexed access type.
Type parameter default
<E = string>: used when no argument is given.
A smell in generics
A type parameter used once, or only in the return type.
Module 13: Types from values
TypeScript112 / 189
Module 13 · Types from values
Say it once, derive the rest
When the same list of statuses is written as an array for the code and again as a union for the types, the two drift apart: someone adds 'damaged' to one and not the other. TypeScript can compute types from values and from other types, so each fact lives in one place. This module is the toolkit.
Note. The right-hand version uses three tools from this module: as const, typeof and [number]. By the end you will read it at a glance.
TypeScript113 / 189
Module 13 · Types from values
typeof in a type gives a value’s type
In code, typeof x is JavaScript’s run-time operator, returning a string like 'number'. In a type position (after :, or on the right of type X =), typeof x is TypeScript’s: the full type of the variable x. The same word, two different jobs, told apart by where it is written.
TypeScript
1const defaults ={loanDays:14,maxBooks:5,finePerDay:0.5}23typeSettings=typeof defaults4// { loanDays: number; maxBooks: number; finePerDay: number }56functionupdate(changes:Partial<Settings>){/* ... */}78if(typeof defaults.loanDays ==='number'){}// the JS one
Partial<T> makes every property optional; it is one of the utility types in the next module.
Type position
A place where a type is expected: after :, inside <>, after type X =.
Value position
A place where code runs: everywhere else.
TypeScript114 / 189
Module 13 · Types from values
keyof and indexed access
From the generics module: keyof T is the union of T’s keys, and T[K] the type at key K. Indexed access takes unions too, and T[number] means "the type of any element" of an array or tuple type.
Writing as const after a literal tells TypeScript not to widen anything: strings stay literal types, arrays become readonly tuples, and every property becomes readonly. It is still an ordinary value at run time; only its type changes.
TypeScript
1const STATUSES =['available','on-loan','lost']asconst2// readonly ['available', 'on-loan', 'lost']34typeStatus=(typeof STATUSES)[number]5// 'available' | 'on-loan' | 'lost'67STATUSES.push('damaged')// error: readonly8for(const s of STATUSES) console.log(s)// still an array
Fill the blanksInteractive · disabled in the PDF
Derive a union of the shelf letters from the array.
The list of genres is the one source. The Genre type and the isGenre check should both come from it, so adding a genre updates all three.
Code challengeInteractive · disabled in the PDF
Make GENRES keep its exact values, derive Genre ('fiction' | 'poetry' | 'history') from it, and keep isGenre compiling and working.
TypeScript
1const GENRES =['fiction','poetry','history']2typeGenre=string34functionisGenre(value:string): value isGenre{5return GENRES.includes(value)6}
TypeScript117 / 189
Module 13 · Types from values
A const object instead of an enum
TypeScript has an enum keyword for named constants, but it generates JavaScript code (it is not just erased), and tools that only strip types cannot run it. A plain object with as const does the same job, and is just JavaScript.
TypeScript
1const Status ={2Available:'available',3OnLoan:'on-loan',4Lost:'lost'5}asconst67typeStatus=(typeof Status)[keyoftypeof Status]8// 'available' | 'on-loan' | 'lost'910functionmark(status:Status){/* ... */}11mark(Status.OnLoan)// fine12mark('lost')// fine too: it is just a string
A value and a type may share a name; TypeScript knows which one you mean from where it is written.
QuizInteractive · disabled in the PDF
In the code above, what is keyof typeof Status?
In the code above, what is keyof typeof Status?
TypeScript118 / 189
Module 13 · Types from values
satisfies checks without widening
An annotation (const x: T = ...) checks the value and replaces its type with T, losing detail. satisfies T (TypeScript 4.9 and later) checks the value against T but keeps the value’s own, more precise type. Use it for configuration objects where you want both a check and exact keys.
TypeScript
1typeRule={ days:number; fine:number}23const annotated:Record<string,Rule>={4book:{days:14,fine:0.5},5dvd:{days:7,fine:1}6}7annotated.magazine // no error: any string key is allowed89const checked ={10book:{days:14,fine:0.5},11dvd:{days:7,fine:1}12}satisfiesRecord<string,Rule>13checked.magazine // error: no such key14typeKind=keyoftypeof checked // 'book' | 'dvd'
Record<K, V> is an object type with keys K and values V.
Find the bugInteractive · disabled in the PDF
satisfies catches one mistake here. Which line?
TypeScript
TypeScript119 / 189
Module 13 · Types from values
ReturnType, Parameters and Awaited
Built-in utility types read types off functions: ReturnType<F> is what F returns, Parameters<F> its parameters as a tuple, and Awaited<T> unwraps a promise. They need a function type, so pass typeof myFunction.
Tip. Deriving from a function is handy when you do not own it, such as a library’s function with no exported type. For your own code, it is usually clearer to name the type and use it in the function.
TypeScript120 / 189
Module 13 · Types from values
Recap
In a type position, typeof x is the type of the value x.
keyof T gives the keys; T[K] a property’s type; T[number] an element’s.
as const keeps literals exact and makes everything readonly.
(typeof obj)[keyof typeof obj] is the union of an object’s values: an enum without enum.
satisfies checks a value against a type while keeping its precise type.
ReturnType, Parameters and Awaited read types off functions.
FlashcardsInteractive · disabled in the PDF
typeof in a type
The type of a variable: type S = typeof settings.
T[number]
The element type of an array or tuple type.
as const
No widening: literal types, readonly tuples, readonly properties.
satisfies T
Check against T, keep the value’s own type.
Annotation vs satisfies
An annotation replaces the type with T; satisfies only checks.
ReturnType<F>
What a function type returns. Use with typeof fn.
Module 14: Utility types and mapped types
TypeScript121 / 189
Module 14 · Utility types and mapped types
Built-in types that transform types
TypeScript ships generic types that take a type and return a changed one. They are called utility types, and they cover most everyday needs: a form where every field is optional, a new record without its id, a lookup table.
Utility
Gives
Example use
Partial<T>
Every property optional
An update with only some fields
Required<T>
Every property required
Settings after defaults are filled in
Readonly<T>
Every property readonly
Data that must not be changed
Pick<T, K>
Only the keys K
A summary: title and author
Omit<T, K>
Every key except K
A new book, before it has an id
Record<K, V>
An object with keys K, values V
Loan rules per item kind
Exclude<U, X> / Extract<U, X>
Union members not matching / matching X
Every status but lost
NonNullable<T>
T without null and undefined
A value after a check
QuizInteractive · disabled in the PDF
Which type describes "a book update that may change the title or pages, and nothing else"?
Which type describes "a book update that may change the title or pages, and nothing else"?
Predict the outputInteractive · disabled in the PDF
Which line is a compile error?
TypeScript
1typeA=Pick<Book,'colour'>// A2typeB=Omit<Book,'colour'>// B
Which line is a compile error?
TypeScript123 / 189
Module 14 · Utility types and mapped types
A mapped type loops over keys
Utility types are written with mapped types: { [K in Keys]: Type } builds an object type with one property per member of the union Keys. With keyof T as the keys and T[K] as the type, it copies T, and you can change each property on the way.
Lines 1-3: For each key K of T, a property K, made optional by ?, with its original type T[K]. This is how Partial is defined.
Lines 5-7: Same keys, but every value is now boolean: a set of on/off switches, one per field.
Lines 9-10: Applied to Book, the loop runs over id, title and author.
Mapped type
{ [K in Keys]: ... }: an object type built by looping over a union of keys.
Utility type
A built-in generic type that transforms other types, like Partial.
TypeScript124 / 189
Module 14 · Utility types and mapped types
Adding and removing ? and readonly
In a mapped type, ? and readonly can be added (+, the default) or removed (-). -? makes optional properties required: that is Required<T>. -readonly makes them writable again.
Fill the blanksInteractive · disabled in the PDF
Write Writable (no readonly) and Complete (nothing optional).
After in, as gives a new name for each key. Mapping a key to never drops it. Combined with Exclude this filters keys; combined with template literal types (two modules from now) it renames them.
What does [K in keyof T as K extends 'id' ? never : K] do?
What does [K in keyof T as K extends 'id' ? never : K] do?
TypeScript126 / 189
Module 14 · Utility types and mapped types
Try it: the fields a form can edit
An edit form shows every field of a record except its id, and lets you change all of them. Write that as one mapped type. The tests are type tests: they compile only when Editable gives exactly the right shape.
Code challengeInteractive · disabled in the PDF
Write Editable<T>: every property of T except 'id', with readonly removed and everything else (types, ?) kept.
TypeScript
1typeEditable<T>=T
TypeScript127 / 189
Module 14 · Utility types and mapped types
Index signatures and Record
For objects used as lookup tables, with keys you do not know in advance, an index signature{ [isbn: string]: Book } says "any string key gives a Book". Record<string, Book> means the same. With a union of known keys, Record<Kind, Rule> requires every one.
TypeScript
1const byIsbn:Record<string,Book>={}2const book = byIsbn['0-00-000000-0']// Book... or is it?34typeKind='book'|'dvd'5const rules:Record<Kind,number>={book:14}6// error: property 'dvd' is missing
Missing keys. By default, reading any key of a Record<string, Book> gives Book, even for keys that are not there. The noUncheckedIndexedAccess setting adds | undefined to such reads (and to array indexes), so you have to check. Turn it on in new projects. A Map is often a better lookup table: its get already returns Book | undefined.
TypeScript128 / 189
Module 14 · Utility types and mapped types
Reading nested types from the inside out
Real types stack utilities. Read them like nested function calls: start with the innermost, work outwards, and write down the result of each step.
TypeScript
1typeDraft=Readonly<Partial<Omit<Book,'id'>>>
Put in orderInteractive · disabled in the PDF
Put the steps of reading Draft in order.
1.Partial<...>: both optional
2.Omit<..., 'id'>: title and author
3.Start from Book: id, title, author
4.Readonly<...>: both optional and readonly
TypeScript129 / 189
Module 14 · Utility types and mapped types
Recap
Partial, Required, Readonly, Pick, Omit, Record cover most object reshaping.
Pick checks its keys; Omit silently ignores unknown ones.
A mapped type { [K in keyof T]: ... } loops over keys.
+/- add or remove ? and readonly.
as renames keys; mapping a key to never drops it.
Turn on noUncheckedIndexedAccess so lookups admit they may miss.
FlashcardsInteractive · disabled in the PDF
Omit<T, K>
T without the keys K. Unknown keys are not an error.
Record<K, V>
An object type with keys K and values V.
Mapped type
{ [K in keyof T]: T[K] }: one property per key.
-?
Removes optionality in a mapped type, as Required does.
Key remapping
[K in keyof T as NewKey]; never drops the key.
Index signature
{ [key: string]: V }: any key of that type gives a V.
noUncheckedIndexedAccess
Adds | undefined to reads through index signatures and array indexes.
Module 15: Conditional types and infer
TypeScript130 / 189
Module 15 · Conditional types and infer
A conditional type is an if/else for types
A extends B ? X : Y reads "if A is assignable to B, the type is X, otherwise Y". It looks like the conditional operator from code, but works on types, and is evaluated by the checker. It is mostly useful inside generics, where A is a type parameter.
TypeScript
1typeIsText<T>=Textendsstring?'yes':'no'23typeA=IsText<'Dune'>// 'yes'4typeB=IsText<412>// 'no'56typeItemOf<T>=Textendsreadonly(inferItem)[]?Item:T7typeC=ItemOf<Book[]>// Book (infer is three slides on)
Conditional type
A extends B ? X : Y: X if A is assignable to B, else Y.
QuizInteractive · disabled in the PDF
What is IsText<'Dune' | 412>?
What is IsText<'Dune' | 412>?
TypeScript131 / 189
Module 15 · Conditional types and infer
Conditional types distribute over unions
When the type before extends is a plain type parameter, a union argument is split up: the conditional runs once per member, and the results are combined in a new union. This is called distribution, and it is how Exclude and Extract work. Members that produce never vanish from the union.
Distribution only happens to a bare type parameter. Wrapping both sides in a one-element tuple, [T] extends [X], checks the union as a whole. Use it when you want to ask a question about the entire type.
Predict the outputInteractive · disabled in the PDF
never is an empty union. Passing never to a distributive conditional gives never: there are no members to run it on. So IsString<never> is never, not true or false. [T] extends [never] is the reliable way to test for never.
TypeScript133 / 189
Module 15 · Conditional types and infer
infer captures part of a type
Inside the extends part, infer Name declares a type variable that TypeScript fills in by matching. If the match succeeds, you can use Name in the "true" branch. It is pattern matching on types, and it is how ReturnType, Parameters and Awaited are written.
TypeScript
1typeMyReturnType<F>=Fextends(...args:any[])=>inferR2?R3:never45typeUnpromise<T>=TextendsPromise<inferV>?V:T67typeA=MyReturnType<()=>Book[]>// Book[]8typeB=Unpromise<Promise<Member>>// Member9typeC=Unpromise<number>// number: no match
...args: any[] matches any parameter list. Using any inside a pattern like this is fine: it is not a value anyone uses.
Since TypeScript 4.7, infer X extends Type only matches when the captured part fits Type. It saves a second nested conditional.
TypeScript
1typeFirstIfText<T>=Textends[inferSextendsstring,...unknown[]]2?S3:never45typeA=FirstIfText<['Dune',412]>// 'Dune'6typeB=FirstIfText<[412,'Dune']>// never
[infer S, ...unknown[]] matches a tuple with at least one element, capturing the first.
TypeScript135 / 189
Module 15 · Conditional types and infer
Try it: unwrap a type
Put infer, distribution and recursion together. Each test is a type, and compiles only when Unwrap gives exactly the type named.
Code challengeInteractive · disabled in the PDF
Unwrap<T> should peel off arrays and promises, as many layers as there are: Unwrap<Promise<Book[]>> is Book. Anything else stays as it is.
TypeScript
1typeUnwrap<T>=T
TypeScript136 / 189
Module 15 · Conditional types and infer
Conditional return types, and their limits
A conditional type can make a function’s return type depend on its argument. The checker cannot always follow the body, though: inside the function, it does not know which branch applies, and a cast is usually needed. Overloads, several signatures for one function, are often clearer.
TypeScript
1functionfind(id:number):Book|undefined2functionfind(ids:number[]):Book[]3functionfind(arg:number|number[]):Book|Book[]|undefined{4return Array.isArray(arg)5? catalogue.filter((b)=> arg.includes(b.id))6: catalogue.find((b)=> b.id === arg)7}89const one =find(1)// Book | undefined10const many =find([1,2])// Book[]
Callers see only the first two signatures. The third, the implementation, must cover both.
QuizInteractive · disabled in the PDF
Can a caller write find(someValue) where someValue: number | number[]?
Can a caller write find(someValue) where someValue: number | number[]?
TypeScript137 / 189
Module 15 · Conditional types and infer
Recap
A extends B ? X : Y chooses a type by assignability.
On a bare type parameter, it distributes over unions; never results vanish.
[T] extends [X] checks the union as a whole.
infer Name captures part of a matched type; infer N extends C constrains it.
Exclude, Extract, ReturnType and Awaited are conditional types.
For functions whose result depends on the argument, overloads are often clearer.
FlashcardsInteractive · disabled in the PDF
Conditional type
A extends B ? X : Y.
Distribution
A conditional on a bare type parameter runs per union member.
Stop distribution
Wrap both sides: [T] extends [X].
infer
Declares a type variable filled in by matching, inside extends.
IsString<never>
never: distributing over an empty union gives nothing.
Overloads
Several call signatures above one implementation; callers see only the overloads.
Module 16: Template literal types
TypeScript138 / 189
Module 16 · Template literal types
Template literal types build strings from types
A template string makes text from values. A template literal type makes string types from types, with the same backtick syntax. A ${string} slot means "any text here". A union in a slot multiplies out: every combination becomes a member.
Predict the outputInteractive · disabled in the PDF
Which values fit Ref? Pick every one that does.
TypeScript
1typeRef=`#${number}`
Which values fit Ref? Pick every one that does.
TypeScript139 / 189
Module 16 · Template literal types
Uppercase, Lowercase, Capitalize, Uncapitalize
Four built-in types change the case of string literal types. They are built into the checker itself (they are called intrinsic types), and work on unions member by member.
Type
Result
Uppercase<'loan'>
'LOAN'
Lowercase<'LOAN'>
'loan'
Capitalize<'loan'>
'Loan'
Uncapitalize<'Loan'>
'loan'
Capitalize<'borrow' | 'return'>
'Borrow' | 'Return'
TypeScript140 / 189
Module 16 · Template literal types
Renaming keys in a mapped type
Template literal types and key remapping (as) together rename every key of a type. keyof T can include numbers and symbols, but Capitalize only takes strings (and a symbol cannot go in a template at all), so K & string keeps only the string keys.
Getters<T> renames keys in types only. A function that makes the name at run time can promise the same exact name in its return type, so getterName('title') is known to be 'getTitle', not just a string.
Code challengeInteractive · disabled in the PDF
Finish getterName(key): return 'get' followed by key with its first letter in capitals, and make its return type that exact name: a template literal type with get and Capitalize<K>.
A common use: one type lists the events and what each carries, and the handler names and their parameter types are derived from it. Add an event once, and its handler appears everywhere it should.
Here the keys are all strings already, so no & string is needed.
TypeScript143 / 189
Module 16 · Template literal types
Matching strings with infer
In a conditional type, a template literal with infer slots is a pattern: TypeScript tries to match a string literal type against it and captures the pieces. When there are several ways to match, each infer takes as little as it can, from left to right.
A conditional type can refer to itself, so it can walk along a string piece by piece. This one collects the :name parts of a path like /members/:memberId/loans/:loanId, the way web routers write them, into a union.
Lines 2: Is there a :name followed by more path? Capture the name and the rest.
Lines 3: If so: this name, joined with the parameters of the rest (the recursion).
Lines 4-5: Otherwise, is there a final :name with nothing after it? That is the last parameter.
Lines 6: No : at all: no parameters.
Find the bugInteractive · disabled in the PDF
With function link<P extends string>(path: P, params: Record<Params<P>, string>), one call fails to compile. Which line?
TypeScript
TypeScript145 / 189
Module 16 · Template literal types
Know where template types stop
They check the form of a string, not its meaning. An email pattern with a ${string} on each side of @ accepts '@' alone; real validation still happens at run time.
Unions multiply: three slots of 50 members each make 125,000 combinations, more than the checker allows for one union (about 100,000).
Only literal types can be matched. A plain string (from user input, say) does not fit a pattern, so the type falls to its "false" branch.
Deep recursion on long strings slows the editor. Keep string parsing for short, fixed strings written in code, like paths and event names.
QuizInteractive · disabled in the PDF
What is Params<string>, using the type from the previous slide?
What is Params<string>, using the type from the previous slide?
TypeScript146 / 189
Module 16 · Template literal types
Recap
Backtick types build string types; unions in slots multiply out.
With as in a mapped type, they rename keys; use K & string.
With infer, they match and split string literal types.
Recursive conditionals can walk a string, such as a route path.
They check form, not meaning, and only help with literals.
FlashcardsInteractive · disabled in the PDF
Template literal type
A string type written in backticks with typed slots, such as LIB- followed by a ${number} slot.
Union in a slot
Produces every combination as a union.
Intrinsic string types
Uppercase, Lowercase, Capitalize, Uncapitalize.
Why K & string?
Keys may be numbers or symbols; Capitalize takes only strings, and symbols cannot go in a template.
How infer matches in a template
Left to right, each slot taking as little as it can.
Module 17: Soundness, brands and type tests
TypeScript147 / 189
Module 17 · Soundness, brands and type tests
TypeScript is not sound, on purpose
A type system is sound if code that compiles can never hold a value of the wrong type at run time. TypeScript is deliberately not: its designers traded a few rare holes for convenience and for describing real JavaScript. Knowing exactly where those holes are is what lets an expert trust the rest.
Escape hatches you write: any, as, !, and type predicates.
Rules relaxed for convenience: method parameters, mutable arrays.
Reads that assume presence: array indexes and index signatures, unless a flag is on.
Anything from outside the program: JSON, the network, libraries with wrong declarations.
Sound
If it compiles, no value ever has the wrong type at run time.
Unsound
Some code compiles even though it can produce a value of the wrong type.
TypeScript148 / 189
Module 17 · Soundness, brands and type tests
Variance: which way assignability flows
'book' is assignable to string. What about types built from them? For things you read (return values, readonly properties), assignability flows the same way: covariance. For things you pass in (parameters) it flows backwards: contravariance. A handler that accepts any string can stand in for a handler of 'book'; not the reverse.
Arrows point from a type to a type it is assignable to.
Predict the outputInteractive · disabled in the PDF
Which assignments compile under strict?
TypeScript
1typeListener=(kind:'book'|'dvd')=>void23declareconstlogAny:(value:string)=>void4declareconstonlyBooks:(kind:'book')=>void56const a:Listener= logAny // A7const b:Listener= onlyBooks // B
Which assignments compile under strict?
Covariant
Assignable in the same direction as the parts: outputs.
Contravariant
Assignable in the opposite direction: inputs.
Invariant
Only the exact same type: things both read and written.
TypeScript149 / 189
Module 17 · Soundness, brands and type tests
Methods are checked loosely
strictFunctionTypes checks function types, but not methods: parameters of members written name(...): ... stay bivariant, accepted in either direction. It was kept for compatibility, since the built-in Array and DOM types rely on it.
Predict the outputInteractive · disabled in the PDF
Which assignment is an error?
TypeScript
1interfaceWithMethod{handle(e:string|number):void}2interfaceWithProp{handle:(e:string|number)=>void}34constonlyText=(e:string)=> e.toUpperCase()56const a:WithMethod={handle: onlyText }// A7const b:WithProp={handle: onlyText }// B
Which assignment is an error?
TypeScript150 / 189
Module 17 · Soundness, brands and type tests
Mutable arrays are covariant, and that's unsound
A mutable array is both read and written, so in theory it should be invariant. TypeScript allows a string[] where a (string | number)[] is expected anyway, for convenience. The hole is real.
TypeScript
1const titles:string[]=['Emma']2const mixed:(string|number)[]= titles // allowed3mixed.push(42)// allowed4titles[1].toUpperCase()// crashes at run time
Tip. Take readonly T[] in parameters you only read. A readonly (string | number)[] has no push, so accepting a string[] there is genuinely safe, and the signature tells callers you will not change their array.
TypeScript151 / 189
Module 17 · Soundness, brands and type tests
Escape hatches, and keeping them honest
Hatch
What it claims
Use it when
x as T
"Trust me, it is a T"
You know more than the checker, and can say why in a comment
x!
"It is not null or undefined"
Rarely: prefer a check that throws with a message
any
"Stop checking this"
Almost never: use unknown and narrow
// @ts-expect-error
"The next line has an error"
In type tests; it fails once the error is gone
// @ts-ignore
"Ignore the next line"
Never: it stays silent after the code changes
Find the bugInteractive · disabled in the PDF
One line hides a real bug behind a cast. Which?
TypeScript
TypeScript152 / 189
Module 17 · Soundness, brands and type tests
Brands: making lookalike types distinct
Structural typing makes every number interchangeable, so a member id can be passed where a book id was meant. A brand intersects the type with a property no real value has. Only values passed through a checking function get it, and it costs nothing at run time.
TypeScript
1typeBookId=number&{readonly __brand:'BookId'}2typeMemberId=number&{readonly __brand:'MemberId'}34functionbookId(value:number):BookId{5if(!Number.isInteger(value)|| value <=0){6thrownewError(`Not a book id: ${value}`)7}8return value asBookId// the one cast, behind the check9}1011declarefunctionlend(book:BookId, to:MemberId):void12declareconst ada:MemberId13lend(ada, ada)// error: MemberId is not BookId14lend(bookId(7), ada)// fine
QuizInteractive · disabled in the PDF
What is a BookId at run time?
What is a BookId at run time?
TypeScript153 / 189
Module 17 · Soundness, brands and type tests
Try it: brand an ISBN
An ISBN is a book’s 13-digit number, kept as text. Make it a brand, so a string that has not been checked cannot be used as one. The last test uses @ts-expect-error: it fails while that line still compiles.
Code challengeInteractive · disabled in the PDF
Brand Isbn (a string with a __brand of 'Isbn'), and make isbn(text) throw unless text is exactly 13 digits (/^\d{13}$/), then return it as an Isbn.
strict is not the strictest setting. These flags close more holes; each costs some code you must write, so turn them on knowingly (all are best turned on at the start of a project).
Flag
What it changes
noUncheckedIndexedAccess
Index reads (obj[key], arr[i]) include undefined
exactOptionalPropertyTypes
a?: string no longer accepts an explicit a: undefined
noImplicitOverride
A method that overrides a parent’s must say override
noPropertyAccessFromIndexSignature
Index-signature keys need obj['key'], not obj.key
noFallthroughCasesInSwitch
A non-empty case must end with break, return or throw
Predict the outputInteractive · disabled in the PDF
With noUncheckedIndexedAccess on, which lines are errors?
TypeScript
1const shelf =['Emma','Dune']2const first:string= shelf[0]// A34for(const title of shelf) title.toUpperCase()// B56const notes:Record<string,string>={}7const note:string= notes.emma ??''// C
With noUncheckedIndexedAccess on, which lines are errors?
TypeScript155 / 189
Module 17 · Soundness, brands and type tests
Testing your types
Complex types are code and deserve tests: that a good use compiles and that a bad one does not. Type tests run when the checker runs; nothing executes. @ts-expect-error asserts that the next line is an error, and complains once it no longer is.
params.test-d.ts
1import{ expectTypeOf, test }from'vitest'23typeEqual<X,Y>=4(<T>()=>TextendsX?1:2)extends5(<T>()=>TextendsY?1:2)?true:false6typeExpect<Textendstrue>=T78type_p=Expect<Equal<Params<'/a/:id/b/:n'>,'id'|'n'>>910test('link needs every parameter',()=>{11expectTypeOf(link).parameter(0).toEqualTypeOf<string>()12// @ts-expect-error: memberId is missing13link('/members/:memberId',{})14})
Equal is a well-known trick for exact type equality. Vitest checks *.test-d.ts files with vitest --typecheck.
Fill the blanksInteractive · disabled in the PDF
Check that a public member type has no passwordHash, and that its shape is exact.
TypeScript
1typePublicMember=Omit<Member,'passwordHash'>23// @ts-: passwordHash must be gone4const leak:PublicMember['passwordHash']=''56expectTypeOf<PublicMember>().<{7 id:number8 name:string9}>()
TypeScript156 / 189
Module 17 · Soundness, brands and type tests
Recap
TypeScript trades soundness for convenience in a few known places.
Outputs are covariant, inputs contravariant; methods are checked bivariantly.
Mutable arrays are covariant: take readonly T[] when you only read.
as unknown as T is the strongest unchecked claim. Check data instead.
Brands make lookalike types distinct at zero run-time cost.
Flags beyond strict, and type tests, close the remaining gaps.
FlashcardsInteractive · disabled in the PDF
Sound type system
Compiling code can never hold a wrong-typed value at run time.
Covariant
Same direction as the parts: return types, readonly data.
Contravariant
Opposite direction: parameter types.
Method bivariance
Method-syntax parameters are checked in both directions: a known hole.
Brand
T & { __brand: 'Name' }: a type only a checking function produces.
@ts-expect-error
Asserts the next line has an error; fails when it does not.
Module 18: tsconfig, modules and declarations
TypeScript157 / 189
Module 18 · tsconfig, modules and declarations
Reading a tsconfig
A tsconfig.json answers four questions: which files to check, what JavaScript they run as, how imports are found, and how strict to be. Here is a typical one for an app whose code is bundled by another tool.
Lines 3-5:target: which JavaScript syntax the output may use. module and moduleResolution: how imports are written and found (two slides on).
Lines 6:lib: which built-in APIs exist. Leave out dom for code with no browser, and document becomes a type error.
Lines 7:types: which global @types/* packages to load. Listing them stops every installed one from loading by default.
Lines 8-9: Strictness: strict, plus safe index reads.
Lines 10-11: Every file must make sense on its own, as a tool that strips types file by file sees it; type imports must say so.
Lines 12-13: Trust library declarations without re-checking them; only check, since the bundler writes the JavaScript.
Lines 15:include: the files to check.
Note.jsonc is JSON that allows comments: tsconfig.json accepts them. A tsconfig can also "extends" another, so many projects share one base file.
TypeScript158 / 189
Module 18 · tsconfig, modules and declarations
What strict turns on
strict: true is a family of flags, and new versions may add members. Each can be switched off on its own, which is how old code bases migrate one flag at a time.
noImplicitAny: a parameter TypeScript cannot type is an error, not any.
strictNullChecks: null and undefined are their own types.
strictFunctionTypes: function types (but not methods) check parameters contravariantly.
strictBindCallApply: bind, call and apply are typed.
strictPropertyInitialization: class fields must be given a value where declared or in the constructor.
noImplicitThis, useUnknownInCatchVariables, alwaysStrict, and since 5.6 strictBuiltinIteratorReturn.
QuizInteractive · disabled in the PDF
Which of these does strict: truenot turn on?
Which of these does strict: truenot turn on?
TypeScript159 / 189
Module 18 · tsconfig, modules and declarations
moduleResolution: bundler or nodenext
moduleResolution tells the checker how to find the file behind an import. It should match whatever really loads the code: a bundler (Vite, esbuild, webpack) or Node itself.
bundler
nodenext
For code loaded by
A bundler
Node directly, or a published library
Relative imports
./fees works
Need the extension: ./fees.js
Package exports
Read, with the import condition
Read, with Node’s conditions
Pair it with
module: "esnext" (or "preserve")
module: "nodenext"
QuizInteractive · disabled in the PDF
Under nodenext, main.ts imports './fees.js', but the file is fees.ts. Why?
Under nodenext, main.ts imports './fees.js', but the file is fees.ts. Why?
TypeScript160 / 189
Module 18 · tsconfig, modules and declarations
import type and verbatimModuleSyntax
A tool that strips types file by file cannot tell whether an imported name is a type (to delete) or a value (to keep) without reading the other file. verbatimModuleSyntax settles it: imports without type are kept exactly as written, so type-only imports must say type.
TypeScript
1importtype{ Book, Member }from'./models.js'2import{ lend,type Loan }from'./loans.js'
import type disappears entirely; an inline type drops one name and keeps the rest.
Find the bugInteractive · disabled in the PDF
With verbatimModuleSyntax on, one line is an error. Which?
TypeScript
TypeScript161 / 189
Module 18 · tsconfig, modules and declarations
Prefer syntax that can simply be erased
Almost everything TypeScript adds is deleted when types are removed. A few older features generate code instead: enum, namespace blocks with values, and constructor parameter properties. Tools that only strip types, such as Node’s built-in support, cannot run them. TypeScript 5.8 added erasableSyntaxOnly to forbid them.
const Status ={Available:0,OnLoan:1}asconstclassShelf{private label:stringconstructor(label:string){this.label = label}}
Note. Numeric enums are also less safe than they look: any number can be assigned to one (since 5.0, only a literal that matches no member is rejected). A union of string literals, or a const object, is the modern choice.
TypeScript162 / 189
Module 18 · tsconfig, modules and declarations
.d.ts files describe JavaScript
A declaration file (.d.ts) has types and no code: declare statements, interfaces and type aliases. It describes JavaScript that exists elsewhere: a library’s compiled output, a global the runtime provides. Libraries written in TypeScript publish them (declaration: true); for JavaScript libraries, the community writes them in @types/* packages.
Types for a JavaScript package that ships none: describe only what you use.
skipLibCheck. With skipLibCheck: true, the checker uses .d.ts files but does not check their insides. Libraries’ declarations sometimes disagree with each other in ways you cannot fix, and checking them is slow. Your own .ts files are still fully checked.
TypeScript163 / 189
Module 18 · tsconfig, modules and declarations
Adding to types you don't own
Interfaces with the same name merge, so you can add to an existing global interface by declaring it again. In a module (a file with import or export), wrap it in declare global.
src/analytics.ts
1interfaceAnalytics{2track:(event:string, data?:Record<string,unknown>)=>void3}45declare global {6interfaceWindow{7 analytics?:Analytics// set by a script tag, maybe8}9}1011window.analytics?.track('book-borrowed')1213export{}
QuizInteractive · disabled in the PDF
A file with no import or export contains declare global { interface Window { ... } }. What happens?
A file with no import or export contains declare global { interface Window { ... } }. What happens?
TypeScript164 / 189
Module 18 · tsconfig, modules and declarations
When the checker gets slow
Type checking costs time in the editor and in CI. Measure before guessing: tsc --extendedDiagnostics prints time and type counts, and tsc --generateTrace <dir> records where the time goes.
Prefer interface ... extends to large & intersections: an interface is one cached type.
Annotate return types of exported functions, so importers do not re-infer large types.
Keep recursive and template literal types shallow; avoid unions of thousands of members.
Since 5.5, isolatedDeclarations requires explicit types on exports, so .d.ts files can be made per file, in parallel.
Put in orderInteractive · disabled in the PDF
Type checking in CI went from 20 to 90 seconds. Put the investigation in order.
1.Run tsc --extendedDiagnostics to confirm checking time is what grew
2.Find the files and types where checking time concentrates
3.Simplify that type, or annotate the exports that use it
4.Measure again and compare with the first numbers
5.Run tsc --generateTrace trace and open the trace
TypeScript165 / 189
Module 18 · tsconfig, modules and declarations
What changed in TypeScript 5.x
Version
What it brought
5.0
const type parameters, moduleResolution: "bundler", verbatimModuleSyntax, standard decorators
5.2
using declarations: resources cleaned up at the end of a scope
5.4
NoInfer<T>; narrowing kept in closures after the last assignment
5.5
Inferred type predicates (filter narrows), isolatedDeclarations
5.6
Errors for checks that are always truthy or always nullish
5.7
rewriteRelativeImportExtensions
5.8
erasableSyntaxOnly, for runtimes that strip types
5.9
import defer, a smaller and stricter tsc --init
Upgrading. Each release can change what compiles. Upgrade on purpose, run the checker across the whole project, and read the "Behavioral changes" part of the release notes first.
TypeScript166 / 189
Module 18 · tsconfig, modules and declarations
Recap
A tsconfig says which files, what output, how imports resolve, and how strict.
strict is a family; noUncheckedIndexedAccess is opt-in on top.
Match moduleResolution to what loads the code: bundler or nodenext.
With verbatimModuleSyntax, type-only imports must say type.
Avoid enum, value namespaces and parameter properties; erasableSyntaxOnly enforces it.
.d.ts files describe JavaScript; declare global and declare module extend types you do not own.
FlashcardsInteractive · disabled in the PDF
lib
Which built-in APIs exist, such as dom.
moduleResolution: "bundler"
Imports are found the way bundlers find them; extensions optional.
verbatimModuleSyntax
Imports are kept as written, so type-only ones must say type.
erasableSyntaxOnly
Forbids TypeScript syntax that generates code, like enum.
Declaration file
A .d.ts: types only, describing JavaScript elsewhere.
skipLibCheck
Use .d.ts files without checking their insides.
Declaration merging
Interfaces with the same name combine into one.
Module 19: Type-level programming
TypeScript167 / 189
Module 19 · Type-level programming
Types can refer to themselves
A type alias may use itself in its own definition, as long as the recursion goes through an object, an array, a conditional type or other structure, never directly (type A = A | string is an error). That is how you describe data of any depth: nested categories in a catalogue, comment threads, or JSON itself.
TypeScript
1typeJson=2|string|number|boolean|null3|Json[]4|{[key:string]:Json}56interfaceCategory{7 name:string8 children:Category[]9}1011functioncountCategories(c:Category):number{12return c.children.reduce(13(total, child)=> total +countCategories(child),14115)16}
reduce walks an array, carrying a running total: here starting from 1, for the category itself.
QuizInteractive · disabled in the PDF
Which values are assignable to Json? Pick every one.
Which values are assignable to Json? Pick every one.
TypeScript168 / 189
Module 19 · Type-level programming
Recursive mapped types reach every level
Readonly<T> only affects the top level: nested objects stay writable. A mapped type that applies itself to each property reaches all the way down. Functions are objects too, so a careful version leaves them alone.
TypeScript
1typeDeepReadonly<T>=2Textends(...args:any[])=>unknown?T3:Textendsobject?{readonly[KinkeyofT]:DeepReadonly<T[K]>}4:T56typeSettings={ loans:{ days:number}; tags:string[]}7declareconst s:DeepReadonly<Settings>8s.loans.days =7// error: read-only9s.tags.push('new')// error: no 'push' on readonly string[]
Arrays survive mapping. A mapped type over keyof T, applied to an array or tuple type, produces an array or tuple again (not an object with numeric keys). That is why DeepReadonly<string[]> becomes readonly string[] without a special case.
TypeScript169 / 189
Module 19 · Type-level programming
Tuples are lists you can compute with
Tuple types have a fixed length you can read with T['length'], and spreads (...) join them, just like arrays in code. Elements can have labels, which only document them. These are the building blocks for typing functions that take or return argument lists.
Each level of type recursion costs the checker work, and it stops with "Type instantiation is excessively deep and possibly infinite" past a limit. Conditional types whose recursive call is the whole result of a branch (tail recursion) are optimised, and can go about 1,000 levels deep. Carry results in an extra type parameter, an accumulator, to make them tail-recursive.
Combining recursion, mapped types and template literals, you can compute every dotted path into an object, such as 'address.city', and type a get(obj, path) that knows what each path leads to.
Lines 1: Only objects have paths; anything else gives never (no further paths).
Lines 3-5: For each string key K: the key itself, or the key, a dot, and any path inside its value.
Lines 6: Index the mapped type with all its keys to collect every property’s union into one.
Lines 9-11: For Member, four paths. When Paths<T[K]> is never, the template with it is never too, and vanishes.
Watch out. This grows fast. On large or recursive types (a Category with children) it never ends, and the checker gives up. Real libraries add a depth limit, often with a tuple counter parameter.
TypeScript172 / 189
Module 19 · Type-level programming
Try it: what a path leads to
Paths<T> lists the paths; the other half of a typed get(obj, path) is the type at the end of one. Walk the path a piece at a time, as Params walked a URL.
Code challengeInteractive · disabled in the PDF
Write PathValue<T, P>: the type found by following the dotted path P into T. PathValue<Member, 'address.city'> is string; a path that does not exist gives never.
TypeScript
1typePathValue<T,Pextendsstring>=unknown
TypeScript173 / 189
Module 19 · Type-level programming
const type parameters infer literals
Generic inference widens: an array of strings is inferred as string[]. Writing const before a type parameter (TypeScript 5.0 and later) asks for the narrowest type, as if the caller had written as const.
TypeScript
1functionshelves<Textendsreadonlystring[]>(names:T){2return names3}4functionexactShelves<constTextendsreadonlystring[]>(names:T){5return names6}78const a =shelves(['A','B'])// string[]9const b =exactShelves(['A','B'])// readonly ['A', 'B']
QuizInteractive · disabled in the PDF
Why is the constraint on exactShelvesreadonly string[] rather than string[]?
Why is the constraint on exactShelvesreadonly string[] rather than string[]?
TypeScript174 / 189
Module 19 · Type-level programming
NoInfer: don't infer from here
When a type parameter appears in several arguments, TypeScript infers it from all of them. Sometimes one argument should only be checked against the others. NoInfer<T> (TypeScript 5.4 and later) marks such a place.
Find the bugInteractive · disabled in the PDF
pick(['fiction', 'poetry'], 'drama') compiles, but it should be an error. Which line needs NoInfer?
TypeScript
TypeScript175 / 189
Module 19 · Type-level programming
Types are read by people
The hardest part of advanced types is not writing them; it is keeping them understandable. Hover output, error messages and review all suffer when a type is clever. A few habits help.
Name intermediate steps: three small aliases beat one dense expression.
Flatten hover output with type Simplify<T> = { [K in keyof T]: T[K] } & {}, which shows an intersection as one object.
Put type tests next to complex types, so changes are checked.
Prefer a slightly looser type with a clear error to a perfect one nobody can debug.
Before computing a type, ask whether a plain union or a little duplicated code is clearer.
Expert note. Library authors spend most of their type effort on the caller’s experience: inference that needs no type arguments, errors that point at the caller’s mistake (often with NoInfer or overloads), and hover output that reads like hand-written types.
TypeScript176 / 189
Module 19 · Type-level programming
Recap
Recursive types describe data of any depth, such as JSON.
Recursive mapped types reach every level; arrays stay arrays.
Tuples have lengths and spreads; infer with ... takes them apart.
Tail-recursive conditionals with an accumulator go deeper and check faster.
const type parameters infer literals; NoInfer stops inference from one place.
Keep types readable: name steps, flatten output, and test them.
FlashcardsInteractive · disabled in the PDF
Recursive type
A type that refers to itself, through an object or array.
T['length']
A tuple’s length as a literal type; number for open-ended arrays.
Tail recursion (types)
The recursive call is the whole branch result; optimised to about 1,000 levels.
Accumulator
An extra type parameter carrying the result so far.
const type parameter
<const T>: infer as if the caller wrote as const.
NoInfer<T>
Check this argument against T, but do not infer T from it.
Module 20: Mastery challenge
TypeScript177 / 189
Module 20 · Mastery challenge
How this challenge works
Each slide is one problem, the kind you meet when you build, debug or review real TypeScript. Work each out on paper or in the playground before answering. If one stumps you, the module it comes from is named in the explanation; go back, then return.
Predict the outputInteractive · disabled in the PDF
This compiles, and prints "All returned". Reviewing it, which line is the real bug?
TypeScript
TypeScript179 / 189
Module 20 · Mastery challenge
Model a to-do item
A to-do item is open, done (with the date it was finished), or snoozed (with the date it comes back). A done item cannot be snoozed. Which type makes impossible states impossible?
QuizInteractive · disabled in the PDF
Pick the best type.
Pick the best type.
TypeScript180 / 189
Module 20 · Mastery challenge
Which statements are true?
QuizInteractive · disabled in the PDF
Pick every true statement.
Pick every true statement.
TypeScript181 / 189
Module 20 · Mastery challenge
Predict the types
Predict the outputInteractive · disabled in the PDF
Everything compiles under strict. Which line makes a later crash possible?
TypeScript
TypeScript184 / 189
Module 20 · Mastery challenge
Follow data from outside to a typed value
Put in orderInteractive · disabled in the PDF
A function loads one member from a server. Put the steps in the order a careful implementation takes them.
1.If response.ok is false, return a failure Result
2.Await fetch for the member’s URL
3.Await response.json() into a variable typed unknown
4.Return a success Result with a value typed Member
5.Check the shape with a type guard or a schema
TypeScript185 / 189
Module 20 · Mastery challenge
Write it: move a loan to its next state
A loan is requested, then approved (it becomes active, with a due date), then returned. next takes a loan and an event and gives the loan’s new state. Every other combination is a mistake, and must throw.
Code challengeInteractive · disabled in the PDF
Finish next(loan, event): approve turns a requested loan into an active one with the event's due; return turns an active loan into a returned one. Anything else throws Cannot <type> a <status> loan.
A generic helper of the kind every codebase grows: put items into groups by a key computed from each one. The key type should come from the callback, so grouping books by genre gives a map keyed by the genres themselves.
Code challengeInteractive · disabled in the PDF
Finish groupBy(items, keyOf): return a Map<K, T[]> from each key to the items that have it, in their original order. Fix the return type too.
Template literal types, recursion and a generic function together. link fills in a path such as /members/:memberId, and its type insists on a value for every :name in it, worked out from the path itself.
Code challengeInteractive · disabled in the PDF
Write Params<Path>, the union of the :name parts of a path, and make link(path, params) replace each :name with params[name]. link('/books/:bookId', { bookId: '7' }) is '/books/7'.
The final test is code of your own. Build a small library catalogue in TypeScript, run by Node, with no any and no as except behind a check.
Hands-onInteractive · disabled in the PDF
TypeScript189 / 189
Module 20 · Mastery challenge
Recap: the course in six lines
A program is instructions; JavaScript runs them, and TypeScript checks them first.
Types describe values and are erased: check outside data at run time.
Model with unions and let narrowing and never prove every case handled.
Derive types from one source with typeof, keyof, as const and satisfies.
Generics, mapped, conditional and template literal types compute types from types.
Know the unsound corners, keep escape hatches rare, and test the types that matter.
FlashcardsInteractive · disabled in the PDF
Where must data be checked at run time?
At the boundary: anything from a server, a file, storage or a user.
Make impossible states impossible
A discriminated union where each state carries only its own data.
Annotation, as or satisfies?
Annotation to fix a type, satisfies to check and keep detail, as only behind a real check.
Why readonly T[] parameters?
Mutable arrays are covariant; readonly ones make that safe.
A type parameter only in the return type
A hidden cast. Return unknown and validate instead.
Testing a type
@ts-expect-error for what must fail, expectTypeOf for exact shapes.
Answer key
1. Your first program · Your first line: console.log · QuizWhat does this print?B. Books: 3The quotes only tell JavaScript where the text begins and ends, so they are not part of what is printed. console.log writes the text itself: Books: 3.
1. Your first program · Instructions run from top to bottom · Put in orderA program checks out a library book in three statements. Put them in the order they should run.1. console.log('Scan the card') 2. console.log('Scan the book') 3. console.log('Print the receipt')Statements run in the order they are written, top to bottom. If you want something to happen first, write it first. Later modules show the few ways to change that order on purpose: conditions, loops and functions.
1. Your first program · An error tells you what, and where · Find the bugThe runtime stops with "ReferenceError: Console is not defined". Which line is wrong?Line 2: console.log('Turn on the lights')JavaScript is case-sensitive: Console and console are different names, and only the lowercase one exists. Notice that line 1 still ran: statements before the error happen, the ones after it do not.
2. Values and variables · Every value has a kind · QuizWhat kind of value is '310', with quotes?B. A stringQuotes make a string, whatever is inside them. '310' is the text made of the characters 3, 1 and 0. 310 without quotes is the number. The difference matters as soon as you add things together, a few slides from now.
2. Values and variables · const never changes, let can · Find the bugThis stops at line 3 with "TypeError: Assignment to constant variable." Which line has to change to fix it?Line 1: let members = 120Line 3 tries to put a new value into a const, so that is where the error is reported. But reassigning is what the program means to do; the mistake is the declaration. A value that has to change is declared with let.
2. Values and variables · Numbers do arithmetic · QuizA late fee is 2 per day plus a fixed 5. What does this print?B. 11Multiplication happens before addition: 3 * 2 is 6, then 5 + 6 is 11. If you guessed 16, you added first; write (5 + daysLate) * 2 when you mean that.
2. Values and variables · Joining text, and template strings · QuizWhat does this print?B. 22One side is a string, so + joins instead of adding: the number becomes the text 2, and the result is the string 22. This is one of the mistakes TypeScript will later help you catch, because it knows which values are strings.
2. Values and variables · Try it: build a sentence · Code challengeSet fullName to the first name, a space and the last name ('Ada Lovelace'), and summary to 'Ada Lovelace has 3 books', using the variables rather than typing the names.One solution:
Solution
1const first ='Ada'2const last ='Lovelace'3const books =345const fullName =`${first}${last}`6const summary =`${fullName} has ${books} books`
A template string puts each value where its ${...} is, so the spaces you type between them are kept. With + it is easy to forget one: first + last gives AdaLovelace. summary can reuse fullName: a variable can be built from another.
2. Values and variables · Comparisons give booleans · Fill the blanksFill in the operators: store 5 in fine, then check whether it is more than 3.1: =, 2: >One = stores a value in a variable. > compares and produces a boolean. Mixing these up (writing = where you meant ===) is one of the most common beginner bugs.
3. Decisions and loops · else if chooses between several cases · QuizWith daysLate set to 3, what does the code above print?B. A little late3 === 0 is false, 3 < 7 is true, so the second block runs. 3 < 30 is true too, but it is never checked: once one branch of a chain runs, the rest are skipped.
3. Decisions and loops · Combine conditions with and, or, not · Fill the blanksA visitor may borrow if they are a member and do not have fines.1: &&, 2: !&& needs both sides to be true, which is what "and" means. ! flips hasFines, so a member with no fines gives true && true. Writing || would let anyone with no fines borrow, member or not.
3. Decisions and loops · Any value can act as a condition · QuizWhat does this print?B. Not available0 is falsy, so the else block runs. Here that happens to be correct, but it is a classic trap when 0 is a real answer (a price of 0, page 0). When you mean a comparison, write it: copiesOnShelf > 0.
3. Decisions and loops · switch compares one value against many · Find the bugWith status set to available, this prints both "On the shelf" and "Borrowed by a member". Which line is missing something?Line 3: console.log('On the shelf'); breakWithout a break after line 3, JavaScript "falls through" into the next case and runs its code too. Every case that should stop needs a break (or a return, as you will see with functions).
3. Decisions and loops · A for loop repeats code a set number of times · QuizHow many lines does this print?B. 4It prints 0, 1, 2 and 3: four lines. When i reaches 4, 4 < 4 is false and the loop stops before printing. Counting from 0 and stopping before the limit is the usual pattern, because lists are numbered from 0 too.
3. Decisions and loops · A while loop repeats until a condition is false · Find the bugThis loop never stops. Which line is wrong?Line 4: page = page + 1The counter goes 1, 0, -1, … and is always <= 10, so the condition never becomes false. A loop needs to move towards its stopping condition on every iteration. If a page freezes, close the tab; in a terminal, Ctrl+C stops a running program.
4. Functions · Parameters are the inputs · QuizWhat does this print?B. 310 has The Hobbit pagesArguments are matched by position, not by meaning: title gets 310 and pages gets the text. JavaScript runs it without complaint. TypeScript would stop this, because you would tell it that pages is a number.
4. Functions · return hands a value back · QuizWhat does this print?B. 10, then undefineddouble prints 10 but returns nothing. A function without return gives back undefined, a special value meaning "no value". So result is undefined, and that is printed second.
4. Functions · Arrow functions are a shorter way to write one · Fill the blanksComplete an arrow function that adds a fine to a balance.1: =>, 2: +=> separates the parameters from the body. With no braces, balance + fine is returned without writing return.
4. Functions · Arrow functions are a shorter way to write one · Find the bugtotal(2, 3) gives undefined instead of 5. Which line is wrong?Line 2: return a + bOnce an arrow function has braces, it is a normal body: it works out a + b and throws the result away. Either add return, or drop the braces: (a, b) => a + b.
4. Functions · Variables live inside their block · QuizWhich of fee, days and total can line 6 use?A. fee onlydays (a parameter) and total exist only inside charge. Outside it, only fee is in scope. To get total out, call the function and use what it returns: charge(3).
4. Functions · Try it: a late fee · Code challengeFinish lateFee(daysLate, perDay = 2): it returns daysLate * perDay, but 0 when the book is not late (daysLate is 0 or less).One solution:
The if handles the special case first and returns early; the last line only runs for books that are late. Without it, lateFee(-2) would give -4, a fee the library pays you. perDay = 2 means callers can leave it out, and lateFee(3, 5) overrides it. Printing the fee with console.log instead of returning it would fail every test: the tests need the value back.
4. Functions · Functions are values too · QuizWhat is the difference between repeat(2, notify) and repeat(2, notify())?B. The first passes the function; the second calls it once now and passes what it returnsBrackets mean "call it now". notify without brackets is the function itself, which repeat can call later. notify() runs immediately and passes its return value (probably undefined), so repeat has nothing to call.
5. Arrays, objects and missing values · An array is an ordered list · QuizAn array has 5 elements. What is the index of the last one?B. 4Counting from 0, five elements have indexes 0 to 4. The last index is always length - 1, so list[list.length - 1] is the last element. Asking for index 5 gives undefined, not an error.
5. Arrays, objects and missing values · for...of visits every element · Fill the blanksAdd up the pages of every book.1: of, 2: +=for (const p of pages) gives you each number in turn. total += p is short for total = total + p. total is a let, because it changes; p can be a const, because each iteration gets a fresh one.
5. Arrays, objects and missing values · map, filter and find take a callback · QuizWhat does this print?A. [6, 20]filter runs first and keeps [3, 10]; map then doubles each one. Methods can be chained like this because each returns a new array you can call the next method on.
5. Arrays, objects and missing values · Try it: fees for late books · Code challengeFinish lateFees(days): keep only the books that are late (more than 0 days), and return the fee for each, 2 per day, in the same order.One solution:
Solution
1functionlateFees(days){2return days.filter((d)=> d >0).map((d)=> d *2)3}
Filter first, then map: once the fees are worked out you can no longer tell a 0 fee from a late book. toEqual compares arrays element by element; toBe would fail even for the right answer, because two separate arrays are never the same array. The original days is untouched, since both methods make new arrays.
5. Arrays, objects and missing values · Arrays of objects are everyday data · QuizWhat does catalogue.find((b) => b.pages > 400).title give, with the catalogue above?A. 'Emma'find returns the first match only, and Emma (474 pages) comes before Dune. filter would return both, as an array.
5. Arrays, objects and missing values · Try it: total a cart · Code challengeFinish cartTotal(cart): return the sum of price * quantity over every line. An empty cart costs 0.One solution:
Solution
1functioncartTotal(cart){2let total =03for(const line of cart){4 total += line.price * line.quantity5}6return total7}
total starts at 0 and grows by one line at a time, which is also why an empty cart gives 0: the loop never runs. It is a let because it changes. A common slip is total += line.price, forgetting the quantity: the second test catches it. Later, cart.reduce((sum, line) => sum + line.price * line.quantity, 0) does the same in one expression.
5. Arrays, objects and missing values · undefined and null mean "nothing here" · Find the bugThis crashes with "Cannot read properties of undefined (reading 'name')". Which line?Line 3: console.log(member ? member.name : 'No such member')No member has id 2, so find returns undefined, and line 3 reads .name of it. Any value that might be missing must be checked before you use it. Remembering that is exactly the kind of job TypeScript takes off your hands.
5. Arrays, objects and missing values · ?. and ?? deal with missing values · QuizA book has fine: 0. What do these give?B. a is 5, b is 0|| falls back on any falsy value, and 0 is falsy, so a is 5: a real fine of 0 is lost. ?? only falls back on null and undefined, so b keeps the 0. Use ?? for defaults.
5. Arrays, objects and missing values · Objects are shared, not copied · QuizWhat does this print?B. 2a and b refer to the same array, so pushing through b changes what a sees. const does not help: it stops a from pointing elsewhere, not the array from changing. const b = [...a] would have made a separate copy.
6. Modules, errors and waiting · Packages are modules other people wrote · QuizWhich import loads a file called members.js sitting next to the current file?B. import { findMember } from './members.js'A path starting with ./ means a file of your own, relative to this one. A bare name like 'members' means an installed package. The name to import goes in braces, and the path is always a string.
6. Modules, errors and waiting · throw signals a problem, catch handles it · QuizIn the code above, why is "Enjoy your book" never printed?B. Because borrow threw, so the rest of the try block was skipped and catch ranStatements in try run normally until one throws. Then JavaScript jumps straight to catch, skipping everything after the throw. Had borrow succeeded, the message would print and catch would be skipped.
6. Modules, errors and waiting · Some work finishes later · QuizIn what order are the letters printed?B. A C BsetTimeout schedules its callback to run after a delay, here 0 milliseconds, but only once the current code has finished. So A and C print first, then B. Asynchronous callbacks never interrupt code that is already running.
6. Modules, errors and waiting · Awaited errors are caught with try...catch · Find the bugThis prints Promise { <pending> } instead of the books. Which line is wrong?Line 3: const books = await response.json()response.json() returns a promise. Without await, books is that pending promise. Any call that returns a promise needs await (or .then) before you use its value. TypeScript can warn you about this, as you will see.
6. Modules, errors and waiting · Try it: count, or fall back · Code challengeFinish countBooks(load): await load() and return how many titles it gave. If it rejects, catch the error and return 0.One solution:
With await inside try, a rejection becomes a thrown error, and catch turns it into 0. Forget the await and two things break: titles is a promise, so titles.length is undefined, and the rejection escapes the try because nothing waited for it there. vi.fn makes a pretend function that records its calls, which is how the first test knows load ran once.
6. Modules, errors and waiting · Promise.all waits for several at once · QuizLoading books takes 2 seconds and loading members takes 3. About how long does the Promise.all version take?B. 3 secondsBoth requests start straight away and run at the same time, so you wait for the slower one: about 3 seconds. Awaiting them one by one takes about 5, because the second only starts when the first has finished.
7. Meet TypeScript · Reading a type error · QuizThe checker says: "Type 'number' is not assignable to type 'string'." Which line could cause it?B. const title: string = 412The first type named is what you gave, the second is what was wanted. A number (412) was given where a string was declared. The third line has it the other way round: "string is not assignable to number".
7. Meet TypeScript · TypeScript works most types out itself · QuizWhat type does TypeScript infer for total?B. total returns numbersum starts as 0, so it is a number, and it is what the function returns: the return type is inferred as number. number[] is the parameter, an array of numbers. You only write a return type when you want the checker to hold the function to it.
7. Meet TypeScript · Try it: annotate, then fix what the checker finds · Code challengeGive lateFee a number parameter and a number return type. Then fix the call the checker now rejects, so fee is 8.One solution:
Once daysLate is a number, lateFee('4') is an error: a string is not assignable to a number. Without the annotation, JavaScript would have run it and given 8 anyway, by quietly turning '4' into a number; with '4' + 2 instead it would have given '42'. The type test checks the function's type exactly; parameter names don't count, only their types.
7. Meet TypeScript · Your editor runs the checker as you type · Find the bugThe editor underlines one line in red. Which one?Line 4: console.log(book.title)TypeScript knows the object's shape from its value, so book.titel gets "Property 'titel' does not exist on type '{ title: string; pages: number; }'. Did you mean 'title'?". Plain JavaScript would have printed undefined and carried on.
7. Meet TypeScript · Always turn strict mode on · QuizWith strict on, what does the checker say about this?B. 'member' is possibly 'undefined'find returns the element type orundefined, and strict null checks make you deal with that before reading .name. This is the crash from the arrays module, caught before running. Without strict, the checker would let it through.
8. Everyday types · Array types: string[] and friends · QuizWhat is the type of prices?B. number[]TypeScript infers an array of the element type: number[]. It does not assume the length stays 3, because arrays can grow. A fixed-length list with a type per position is a tuple, which you write on purpose: [string, number].
8. Everyday types · An object type lists the properties · Fill the blanksGive member a type: a numeric id and a name.1: number, 2: stringEach property gets its own type. greet({ id: 1, name: 'Ada' }) now compiles, and greet({ id: '1', name: 'Ada' }) is rejected, because a string id is not a number.
8. Everyday types · Optional and readonly properties · QuizIn heading, why does book.subtitle.toUpperCase() (without ?.) fail to compile?B. Because subtitle is string | undefined, and undefined has no toUpperCaseAn optional property might be absent, so its type includes undefined. The checker refuses a call that would crash for that case. ?. (or an if) handles it, exactly as you did in plain JavaScript, except now you cannot forget.
8. Everyday types · Typing functions and callbacks · Find the bugOne line fails to compile. Which?Line 3: return 0The return type says number, so every return must give one: "Type 'string' is not assignable to type 'number'". Annotating the return type is how you ask the checker to hold a function to its promise. Without it, the inferred type would be number | 'none', and the problem would surface wherever it was used.
8. Everyday types · Saying a value may be missing · QuizWhich declaration compiles with strict on?B. let borrower: string | null = nullOnly when the type includes null can the variable hold it. This is the whole point of strict null checks: every place that might have nothing says so in its type, and every use of it is checked. The | is a union type, the subject of the next module.
8. Everyday types · Try it: a label for a book · Code challengeFinish label(book): 'Dune by Frank Herbert', or with a subtitle, 'Dune: Desert Planet by Frank Herbert'.One solution:
Solution
1interfaceBook{2 title:string3 author:string4 subtitle?:string5}67functionlabel(book:Book):string{8const name = book.subtitle9?`${book.title}: ${book.subtitle}`10: book.title11return`${name} by ${book.author}`12}
book.subtitle is string | undefined: it may be missing, and when it is, the condition picks the title alone. Always adding : and the subtitle compiles too (a template string accepts undefined), but gives Dune: undefined by Frank Herbert, which is what the first test catches. Types catch a lot, not everything.
9. Unions and narrowing · A union is one of several types · QuizWhich lines compile? Pick every one that does.A. A; C. CStrings and numbers both have toString, and both can go in a template string, so A and C are fine. Only strings have toUpperCase: if id is a number, B would crash, so it is an error until you check which one you have.
9. Unions and narrowing · Every everyday check narrows · QuizWhat is wrong with this function?B. A fine of 0 is reported as "No fine recorded"It compiles, and the truthiness check removes undefined as intended. But 0 is falsy too, so a real fine of 0 falls into the second branch. When 0 or '' are meaningful, check what you mean: fine !== undefined.
9. Unions and narrowing · Discriminated unions: a tag says which shape · QuizOn the last line of describe, what is the type of loan?B. The 'returned' member onlyEach if returned, so by the last line TypeScript has removed 'active' and 'lost' from the union, leaving only 'returned'. That is why loan.returnedOn is allowed there.
9. Unions and narrowing · Make the checker prove every case is handled · Find the bugSomeone adds | { status: 'damaged'; bookId: number } to Loan. Which line of fee now fails to compile?Line 9The 'damaged' member reaches default, so loan there is that member, not never: Type '{ status: "damaged"; bookId: number; }' is not assignable to type 'never'. The error points at exactly the place to add the new case. Without this line, the function would quietly throw at run time.
9. Unions and narrowing · Try it: a fee for every kind of loan · Code challengeFinish fee(loan): an active loan costs 2 per daysLate, a returned one costs 0, and a lost one costs its price plus 5.One solution:
Inside each case, loan is narrowed to one member, so loan.daysLate is allowed only under 'active'. Reading it before the switch is an error, because a returned loan has no daysLate. With every case returning, the checker knows the end of the function is unreachable, so the number return type is satisfied without a default. Add a fourth status and it will say the function can end without returning.
9. Unions and narrowing · Write your own type guards · Fill the blanksComplete a guard that tells the checker a value is a string.1: is, 2: typeofvalue is string is the predicate; the body must actually check it. The checker trusts the predicate without verifying the body, so a guard that returns true wrongly is a lie it will believe. Keep guards short and obvious.
10. Shapes, classes and escape hatches · Fresh object literals are checked for extras · QuizWhich line is an error?A. AA writes the literal straight into a Member, so nmae is reported as an excess property (and probably a typo for name). B passes a variable that already exists; its extra joined is allowed by structural typing, because the object could be used for other things too.
10. Shapes, classes and escape hatches · Try it: a shelf that fills up · Code challengeFinish add(title): put the title on the shelf and return true, unless the shelf already holds capacity books; then add nothing and return false.One solution:
Inside a method, this is the shelf it was called on, so this.books and this.capacity belong to that one shelf. The check comes before the push: pushing first and then returning false would leave the extra book on the shelf. Use >=, not >: with >, a shelf of 1 would take a second book.
10. Shapes, classes and escape hatches · Intersections combine types · QuizWhat is type Odd = 'active' & 'lost'?C. neverNo value can be both strings at once, so the intersection is empty: never. Intersections of object types add up properties; intersections of conflicting primitives are impossible. A union (|) is either one; an intersection (&) is both.
10. Shapes, classes and escape hatches · as tells the checker to trust you · Find the bugEverything compiles, but one line makes a later crash possible. Which?Line 2: const parsed: unknown = JSON.parse(text) // then check itThe as claims the JSON has a numeric loanDays, and the checker believes it. With '{}', loanDays is undefined and line 5 crashes. Parse into unknown and check the shape (by hand, or with a validation library such as Zod), so the type is earned rather than asserted.
11. Typing async code, data and errors · An async function returns a Promise<T> · QuizWhat does the checker say about this?B. Property 'length' does not exist on type 'Promise<Book[]>'Without await, books is the promise, and promises have no length. This is the missing-await bug from the foundations, now caught before running. The checker cannot catch a promise you ignore entirely, though: a linter rule such as typescript-eslint's no-floating-promises does that.
11. Typing async code, data and errors · A validating function earns the type · QuizWhy annotate data as unknown rather than leaving it as any?B. With any, a forgotten check would still compile; with unknown it cannotany would let return data compile with no check at all. unknown makes the checker insist on narrowing first, so removing the if by mistake becomes a compile error. (response.json() itself returns Promise<any>, which is the reason to annotate.)
11. Typing async code, data and errors · fetch does not fail on an error status · Find the bugWhen the book does not exist, the server answers 404 with {"error": "Not found"}. Which line should have been there to stop this from being treated as a book?Line 3: if (!response.ok) throw new Error(HTTP ${response.status})Line 3 only logs the status; nothing stops a 404 body from being returned as if it were a book. Check response.ok and throw (or return a failure) before reading the body as data.
11. Typing async code, data and errors · A caught error is unknown · Fill the blanksNarrow the caught value before reading its message.1: instanceof, 2: Errorinstanceof Error narrows unknown to Error, which has message. Errors you create should always be new Error(...) (or a class extending it), so they carry a message and a stack trace.
11. Typing async code, data and errors · Return failures as values when callers must handle them · QuizRight after const result = borrow(dune, ada), why is result.value an error?B. Because result might be the failure member, which has no valueUntil you check result.ok, result is either member of the union, and only one has value. The checker makes the caller deal with failure first, which is the point of the pattern. Keep throw for real surprises: bugs and broken infrastructure.
11. Typing async code, data and errors · Try it: parse without throwing · Code challengeFinish parsePages(text): a success with the number for text like '310'; a failure with the error 'Not a number' when Number(text) is NaN, or 'Must be more than 0' when it is 0 or less.One solution:
Solution
1typeResult<T>=2|{ ok:true; value:T}3|{ ok:false; error:string}45functionparsePages(text:string):Result<number>{6const pages =Number(text)7if(Number.isNaN(pages))return{ok:false,error:'Not a number'}8if(pages <=0)return{ok:false,error:'Must be more than 0'}9return{ok:true,value: pages }10}
Each return must match one member of the union exactly: { ok: true, error: '...' } would not compile, because the success member has no error. Check for NaN with Number.isNaN, never pages === NaN: NaN is not equal to anything, itself included. And the order matters: NaN <= 0 is false, so without the first check 'many' would slip through as a success.
12. Generics · A type parameter is a variable for a type · QuizWhat is the type of pair?A. [string, number]Each type parameter is inferred from its own argument: A from 'Dune' (widened to string) and B from 412 (number). The return type [A, B] becomes the tuple [string, number].
12. Generics · extends says what T must have · Find the bugLine 2 fails to compile. Which line has to change to fix it?Line 1: function titles<T extends { title: string }>(items: T[]): string[] {Without a constraint, T might be number, which has no title: "Property 'title' does not exist on type 'T'". The error shows on line 2, but the fix is on line 1: constrain T to { title: string }. (Here you could also drop the generic and take { title: string }[], since T is never returned.)
12. Generics · K extends keyof T: a key of the object · Fill the blanksType a function that reads one property of an object.1: extends, 2: keyofK extends keyof T limits the key to real property names, and T[K] returns that property’s type. get(dune, 'pages') is a number; get(dune, 'x') does not compile.
12. Generics · Try it: look things up by a key · Code challengeFinish indexBy(items, key): give it a second type parameter K for the key, return a Map<T[K], T>, and fill it with each item under its key property.One solution:
Solution
1functionindexBy<T,KextendskeyofT>(2 items:T[],3 key:K4):Map<T[K],T>{5const index =newMap<T[K],T>()6for(const item of items) index.set(item[key], item)7return index8}
K links the key argument to the result: called with 'isbn', K is 'isbn' and T[K] is string, so the map's keys are strings. With key: keyof T alone, the function cannot say which key it got, so the best it can do is T[keyof T], every property type mixed together. Passing new Map<T[K], T>() its types up front saves annotating index.
12. Generics · Types and interfaces can be generic too · QuizIn Result<Loan>, what is E?B. stringThe default E = string is used when no second argument is passed. Result<Loan, Error> would replace it. Defaults keep common uses short while leaving room for the rare one.
12. Generics · A good type parameter links two places · QuizWhich signature is really a hidden type assertion?B. function parse<T>(json: string): Tparse<Book>(text) returns a Book with no check: T only appears in the output, so the caller asserts the type. Return unknown instead and validate. The third is harmless but pointless: T is used once, so unknown[] says the same.
13. Types from values · keyof and indexed access · QuizWhat is Shelf?B. stringtypeof catalogue is { title: string; shelf: string }[]; [number] takes the element; ['shelf'] takes that property: string. It is not 'A' | 'D' because object properties are widened: they could be changed later. The next slide fixes that.
13. Types from values · as const keeps values exact and readonly · Fill the blanksDerive a union of the shelf letters from the array.1: as const, 2: typeofWithout as const, SHELVES is string[] and ShelfLetter would be plain string. With it, the element type is 'A' | 'B' | 'C', and adding 'D' to the array adds it to the type automatically.
13. Types from values · Try it: one list, a type and a check · Code challengeMake GENRES keep its exact values, derive Genre ('fiction' | 'poetry' | 'history') from it, and keep isGenre compiling and working.One solution:
Solution
1const GENRES =['fiction','poetry','history']asconst2typeGenre=(typeof GENRES)[number]34functionisGenre(value:string): value isGenre{5return GENRES.some((genre)=> genre === value)6}
After as const, GENRES.includes(value) stops compiling: includes on a tuple of literals only accepts those literals, and value is any string. That is the checker being strict, not wrong. some with === compares a genre to any string, so it compiles and needs no cast; (GENRES as readonly string[]).includes(value) also works, because widening a type is safe.
13. Types from values · A const object instead of an enum · QuizIn the code above, what is keyof typeof Status?B. 'Available' | 'OnLoan' | 'Lost'typeof Status is the object’s type; keyof takes its keys, the capitalised names. Indexing the type with all its keys, T[keyof T], gives the union of its values. That pattern is worth memorising.
13. Types from values · satisfies checks without widening · Find the bugsatisfies catches one mistake here. Which line?Line 3: dvd: { days: 7, fine: 1 },fien is an excess property and fine is missing, so the dvd entry does not satisfy Rule. Without satisfies (and without an annotation), the typo would compile and the fine would be undefined at run time.
14. Utility types and mapped types · Built-in types that transform types · QuizWhich type describes "a book update that may change the title or pages, and nothing else"?A. Partial<Pick<Book, 'title' | 'pages'>>Pick keeps only title and pages; Partial then makes both optional. Utilities compose like functions, read from the inside out. Omit would do the opposite: everything except those two.
14. Utility types and mapped types · Pick checks its keys, Omit does not · QuizWhich line is a compile error?A. APick<T, K extends keyof T> requires real keys, so 'colour' is rejected. Omit's key parameter is only K extends keyof any (any string, number or symbol), so a misspelled key is silently ignored and nothing is removed. Double-check the keys you Omit, especially after renaming a property.
14. Utility types and mapped types · Adding and removing ? and readonly · Fill the blanksWrite Writable (no readonly) and Complete (nothing optional).1: -readonly, 2: -?-readonly goes before the brackets, where readonly would go; -? goes after them, where ? would go. Complete is the built-in Required. Removing ? also removes the undefined it added.
14. Utility types and mapped types · Renaming and dropping keys with as · QuizWhat does [K in keyof T as K extends 'id' ? never : K] do?B. Removes the id key; keeps the restA key remapped to never is left out of the result; the value type is untouched. That is different from writing never as a property type, which keeps the key but makes it impossible to set. The ? : in a type is a conditional type, the subject of the next module.
14. Utility types and mapped types · Try it: the fields a form can edit · Code challengeWrite Editable<T>: every property of T except 'id', with readonly removed and everything else (types, ?) kept.One solution:
as Exclude<K, 'id'> renames id to never, which drops it, and leaves every other key alone. -readonly removes the modifier; ? is kept because the mapping still walks keyof T (it is homomorphic), so modifiers you do not mention carry over. Omit<Book, 'id'> gets the keys right but keeps readonly, which is the usual first attempt.
14. Utility types and mapped types · Reading nested types from the inside out · Put in orderPut the steps of reading Draft in order.1. Start from Book: id, title, author 2. Omit<..., 'id'>: title and author 3. Partial<...>: both optional 4. Readonly<...>: both optional and readonlyEach utility receives the result of the one inside it. In your editor, hovering over Draft shows the final shape, but only reading inside-out tells you why it has that shape.
15. Conditional types and infer · A conditional type is an if/else for types · QuizWhat is IsText<'Dune' | 412>?C. 'yes' | 'no'Surprising at first: when the checked type is a bare type parameter and it receives a union, the condition is applied to each member separately, and the results are joined. 'Dune' gives 'yes', 412 gives 'no'. The next slide explains this rule.
15. Conditional types and infer · Conditional types distribute over unions · Fill the blanksWrite NotEmpty: T without null and undefined.1: never, 2: TDistribution runs the check on each member: null and undefined become never and disappear; everything else is kept. NotEmpty<string | null> is string. The built-in is NonNullable (written today as T & {}, which has the same effect).
15. Conditional types and infer · Wrap it in brackets to stop distribution · QuizWhat are A and B?A. A is boolean, B is falseA distributes: 'a' gives true, 1 gives false, and true | false is boolean. B checks the whole union 'a' | 1 against string; it does not fit, so false.
15. Conditional types and infer · infer captures part of a type · Fill the blanksComplete both types with infer.1: infer A, 2: infer Einfer can stand anywhere a type could: a parameter, an array element, a property. FirstArg<(id: number) => void> is number; ElementOf<string[]> is string. readonly in the pattern makes it match both readonly and ordinary arrays.
15. Conditional types and infer · Try it: unwrap a type · Code challengeUnwrap<T> should peel off arrays and promises, as many layers as there are: Unwrap<Promise<Book[]>> is Book. Anything else stays as it is.One solution:
Each branch captures what is inside with infer and unwraps that again, so the layers come off one at a time until neither pattern matches. T is a bare type parameter, so a union is unwrapped member by member: string[] | number gives string | number. Returning E instead of Unwrap<E> passes the simple tests but leaves Book[] in the layered one.
15. Conditional types and infer · Conditional return types, and their limits · QuizCan a caller write find(someValue) where someValue: number | number[]?B. No: callers only see the overloads, and neither takes the unionThe implementation signature is hidden from callers. Each overload is tried in order, and neither accepts number | number[]. Add a third overload for the union if callers need it.
16. Template literal types · Template literal types build strings from types · QuizWhich values fit Ref? Pick every one that does.A. '#12'; C. '#1.5'After the #, a ${number} slot accepts any text that reads as a number, so '#12' and '#1.5' fit. An empty slot is not a number, so '#' fails, and '12' is missing the # the pattern starts with.
16. Template literal types · Renaming keys in a mapped type · Fill the blanksWrite Setters: setName(value: string): void and so on.1: Capitalize, 2: T[K]Capitalize turns name into Name, so the key becomes setName. The value type is a function taking the original property type, T[K]. The key changed, but K still refers to the original key, so T[K] still works.
16. Template literal types · Try it: the same name, in code and in types · Code challengeFinish getterName(key): return 'get' followed by key with its first letter in capitals, and make its return type that exact name: a template literal type with get and Capitalize<K>.One solution:
Solution
1typeGetterName<Kextendsstring>=`get${Capitalize<K>}`23functiongetterName<Kextendsstring>(key:K):GetterName<K>{4const first = key.charAt(0).toUpperCase()5const name =`get${first}${key.slice(1)}`6return name asGetterName<K>7}
The type and the code describe the same transformation, but the checker cannot follow toUpperCase and slice: to it, the template string is only a string. So the one as sits right where the value is built, with tests to back it. This is the usual shape for typed string helpers: compute in code, state the result in a type, check both with tests.
16. Template literal types · Matching strings with infer · QuizWhat is R?B. '42/loans'The ${string} before the first / takes as little as possible: 'members'. The infer R then gets everything after that first slash, '42/loans'. To get the last piece, you would recurse: apply Rest again until no / is left.
16. Template literal types · Recursion: the parameters of a path · Find the bugWith function link<P extends string>(path: P, params: Record<Params<P>, string>), one call fails to compile. Which line?Line 3: link('/members/:memberId', { memberId: '1' })Params<'/members/:memberId'> is 'memberId', so the object must have exactly that key; member is a typo the checker now catches. The last line works because Record<never, string> is an empty object type.
16. Template literal types · Know where template types stop · QuizWhat is Params<string>, using the type from the previous slide?A. neverNot every string contains a :, so string is not assignable to either pattern, and the result is never: no parameters at all. link(somePath, {}) would then compile whatever the path held. Template types shine on literals written in the code, not on values only known at run time.
17. Soundness, brands and type tests · Variance: which way assignability flows · QuizWhich assignments compile under strict?A. Only AA Listener will be called with 'book' or 'dvd'. logAny handles any string, so A is safe. onlyBooks would receive a 'dvd' it cannot handle, so B is rejected (by strictFunctionTypes, part of strict). Wider parameters make the more capable function.
17. Soundness, brands and type tests · Methods are checked loosely · QuizWhich assignment is an error?B. BBoth are unsafe: handle(42) would call toUpperCase on a number. Only B is caught, because its member is a property with a function type. A uses method syntax, checked bivariantly. For callbacks in your own types, prefer property syntax: onReturn: (e: ReturnEvent) => void.
17. Soundness, brands and type tests · Escape hatches, and keeping them honest · Find the bugOne line hides a real bug behind a cast. Which?Line 6: return parseLoans(body) // checks, then returns Loan[]as unknown as T converts anything to anything: the strongest claim TypeScript lets you make, with no evidence. An error body from a 401 would be returned as Loan[]. Line 5 is fine: going up to unknown is always safe. Check on the way down.
17. Soundness, brands and type tests · Brands: making lookalike types distinct · QuizWhat is a BookId at run time?C. A plain number: the brand exists only in typesThe __brand property is never created; it is a type-level marker that makes plain numbers unassignable. Zero run-time cost, but the guarantee is only as good as the function that does the cast. Validation libraries such as Zod offer the same idea after a check.
17. Soundness, brands and type tests · Try it: brand an ISBN · Code challengeBrand Isbn (a string with a __brand of 'Isbn'), and make isbn(text) throw unless text is exactly 13 digits (/^\d{13}$/), then return it as an Isbn.One solution:
Solution
1typeIsbn=string&{readonly __brand:'Isbn'}23functionisbn(text:string):Isbn{4if(!/^\d{13}$/.test(text)){5thrownewError(`Not an ISBN: ${text}`)6}7return text asIsbn8}
The intersection adds a property no string has, so a plain string is no longer assignable to Isbn, and the only way to get one is through isbn. The as Isbn is safe because it comes after the check. Without ^ and $, the pattern would find 13 digits inside a longer string and let the second case through.
17. Soundness, brands and type tests · Flags beyond strict · QuizWith noUncheckedIndexedAccess on, which lines are errors?A. Ashelf[0] is now string | undefined (the array could be empty), so A fails. for...of only visits elements that exist, so B is fine. C handles the undefined with ??.
17. Soundness, brands and type tests · Testing your types · Fill the blanksCheck that a public member type has no passwordHash, and that its shape is exact.1: expect-error, 2: toEqualTypeOf@ts-expect-error fails the check if the line compiles, here if passwordHash came back. @ts-ignore would stay silent forever. toEqualTypeOf checks exact equality, not just assignability.
18. tsconfig, modules and declarations · What strict turns on · QuizWhich of these does strict: truenot turn on?D. noUncheckedIndexedAccessnoUncheckedIndexedAccess changes the type of every index read, which would break too much existing code to include in strict. You opt in. Since 5.9, the tsconfig that tsc --init writes turns it on, along with exactOptionalPropertyTypes.
18. tsconfig, modules and declarations · moduleResolution: bundler or nodenext · QuizUnder nodenext, main.ts imports './fees.js', but the file is fees.ts. Why?B. The import names the file as it will run: tsc writes fees.js, and Node needs the exact pathBy default TypeScript does not rewrite import paths, and Node does not guess extensions, so the source names the output file; the checker maps ./fees.js back to fees.ts. (Since 5.7, rewriteRelativeImportExtensions lets you write ./fees.ts and have it rewritten.) Under bundler the extension is optional, because the bundler resolves it.
18. tsconfig, modules and declarations · import type and verbatimModuleSyntax · Find the bugWith verbatimModuleSyntax on, one line is an error. Which?Line 2: import { lend, type Loan } from './loans.js'Loan is a type: it does not exist in the JavaScript. Kept verbatim, the import would ask loans.js at run time for a Loan export that is not there, so the compiler insists on type. The inline form keeps the import of lend, a real function.
18. tsconfig, modules and declarations · Adding to types you don't own · QuizA file with no import or export contains declare global { interface Window { ... } }. What happens?B. An error: global augmentations must be inside a moduleIn a script file (no imports or exports), everything at the top level is already global, so declare global is an error. Either drop the wrapper or make the file a module, for example with export {}, as above. (With moduleDetection: "force", which tsc --init writes, every file is a module, so the wrapper is needed.)
18. tsconfig, modules and declarations · When the checker gets slow · Put in orderType checking in CI went from 20 to 90 seconds. Put the investigation in order.1. Run tsc --extendedDiagnostics to confirm checking time is what grew 2. Run tsc --generateTrace trace and open the trace 3. Find the files and types where checking time concentrates 4. Simplify that type, or annotate the exports that use it 5. Measure again and compare with the first numbersMeasure, locate, change one thing, measure again. Slow checks usually come from one or two expensive types (a deep conditional, a huge union) that every importer pays for, so the trace points to a small fix with a large effect.
19. Type-level programming · Types can refer to themselves · QuizWhich values are assignable to Json? Pick every one.A. { title: 'Dune', tags: ['sf'] }; B. [1, [2, [3]]]Objects and arrays nest to any depth, so the first two fit. A Date is an object with methods, not a JSON value, and undefined is not part of JSON at all: JSON.stringify drops or changes both. The type says exactly what survives a round trip through JSON.
19. Type-level programming · Tuples are lists you can compute with · QuizWhat is Concat<['a'], string[]>['length']?C. number[...['a'], ...string[]] is ['a', ...string[]]: at least one element, but no fixed length. So its length is number. Lengths are literal only for tuples of known size.
19. Type-level programming · Recursion has limits: keep it tail-recursive · Fill the blanksComplete Join: Join<['a', 'b', 'c'], '/'> is 'a/b/c'.1: infer R, 2: JoinSplit off the first element, then recurse on the rest with Join<R, Sep>. infer R extends string[] keeps R usable as an argument to Join. A one-element tuple ends the recursion without a trailing separator; the empty tuple gives ''.
19. Type-level programming · Try it: what a path leads to · Code challengeWrite PathValue<T, P>: the type found by following the dotted path P into T. PathValue<Member, 'address.city'> is string; a path that does not exist gives never.One solution:
Split at the first dot: if the head is a key of T, recurse into T[Head] with the rest; with no dot left, the path is a single key. The Head extends keyof T check is what turns a wrong path into never rather than an error, and indexing T[Head] is only allowed after it. The recursive call is the whole branch, so it is tail-recursive and cheap. A common slip: forgetting the no-dot case, which makes every one-key path never.
19. Type-level programming · const type parameters infer literals · QuizWhy is the constraint on exactShelvesreadonly string[] rather than string[]?B. A const type parameter infers a readonly tuple, and a readonly constraint is what reliably accepts itThe literal is inferred as readonly ['A', 'B'], which a readonly string[] constraint accepts as it is. A mutable string[] constraint cannot accept a readonly tuple: TypeScript 5.0 fell back to plain string[] and lost the literals; TypeScript 5.9 drops the readonly instead, inferring ['A', 'B']. readonly also promises callers the function will not change their array, and it accepts mutable arrays too.
19. Type-level programming · NoInfer: don't infer from here · Find the bugpick(['fiction', 'poetry'], 'drama') compiles, but it should be an error. Which line needs NoInfer?Line 3: initial: NoInfer<T>Without NoInfer, T is inferred from both arguments, as 'fiction' | 'poetry' | 'drama', so anything passes. With initial: NoInfer<T>, T comes only from options, and 'drama' is rejected.
20. Mastery challenge · How this challenge works · QuizWhat does this print?B. Dune: 2copy and shelf are the same object. 0 || 2 is 2 because 0 is falsy, so copies becomes 2 on the shared object. 2 ?? 5 is 2, since ?? only replaces null and undefined. const stops reassigning shelf, not changing it. (Values and variables; arrays and objects.)
20. Mastery challenge · Async under pressure · Find the bugThis compiles, and prints "All returned". Reviewing it, which line is the real bug?Line 3: await markReturned(id)Nothing waits for markReturned, so "All returned" prints before any loan is saved, and if one fails, its rejection is never handled. The checker does not flag an ignored promise; a linter rule for floating promises (such as typescript-eslint's no-floating-promises) does. await Promise.all(loanIds.map(markReturned)) would also run them at the same time. (Modules, errors and waiting; async and errors.)
20. Mastery challenge · Model a to-do item · QuizPick the best type.B. { title: string } & ({ state: 'open' } | { state: 'done'; doneOn: Date } | { state: 'snoozed'; until: Date })The first allows done and snoozed both true, and a done item with no date. The third allows a done item without a date, and cannot say which date means what. The discriminated union gives each state exactly its own data, and a switch on state with a never check covers every case. (Unions and narrowing.)
20. Mastery challenge · Which statements are true? · QuizPick every true statement.B. unknown can hold any value, but must be narrowed before use.; D. satisfies checks a value without replacing its inferred type.Types are erased, so nothing is checked at run time (meet TypeScript). Omit accepts any key (mapped types). The checker trusts a type predicate without checking its body (unions and narrowing). The two true ones are the heart of handling outside data and configuration safely.
20. Mastery challenge · Predict the types · QuizWhat are A, B and C?B. A is 14 | 7; B is 'list' | 'one'; C is 'getBook' | 'getDvd' | 'setBook' | 'setDvd'as const keeps 14 and 7 as literals, and indexing with all keys gives their union (types from values). Box distributes over the union: string[] gives 'list', number gives 'one' (conditional types). Two unions in a template multiply out to four members (template literal types).
20. Mastery challenge · Write it: keep the properties of one type · Fill the blanksComplete PickByValue.1: as, 2: V, 3: neveras renames each key; a key renamed to never is dropped. The condition T[K] extends V keeps a key when its value type fits V. The value stays T[K]. Note that here T[K] is not a bare type parameter, so a union property type is checked as a whole, not distributed.
20. Mastery challenge · Spot the unsound line · Find the bugEverything compiles under strict. Which line makes a later crash possible?Line 6Mutable arrays are covariant: a list of loans with due is accepted where a list of { bookId } is expected, and addOverdue pushes an element without due. The last line then crashes on it. The call on line 6 is where the hole opens. A readonly { bookId: number }[] parameter would make the push impossible inside addOverdue (soundness).
20. Mastery challenge · Follow data from outside to a typed value · Put in orderA function loads one member from a server. Put the steps in the order a careful implementation takes them.1. Await fetch for the member’s URL 2. If response.ok is false, return a failure Result 3. Await response.json() into a variable typed unknown 4. Check the shape with a type guard or a schema 5. Return a success Result with a value typed MemberStatus first (fetch fulfils on 404), then the body as unknown, then a real check that earns the type, and only then a typed value, wrapped so callers must handle failure. Every step comes from the async and errors module; skipping any one is a bug you have now seen.
20. Mastery challenge · Write it: move a loan to its next state · Code challengeFinish next(loan, event): approve turns a requested loan into an active one with the event's due; return turns an active loan into a returned one. Anything else throws Cannot <type> a <status> loan.One solution:
Narrow both unions in one condition: after event.type === 'approve', event.due is allowed. Build each new state from scratch. The tempting { ...loan, status: 'returned' } compiles, because spread properties escape the excess-property check, but it carries the old due into a returned loan, and toEqual catches it. A returned loan with a due date is exactly the impossible state the union was meant to rule out. (Unions and narrowing; shapes and escape hatches.)
20. Mastery challenge · Write it: group a list, keeping the types · Code challengeFinish groupBy(items, keyOf): return a Map<K, T[]> from each key to the items that have it, in their original order. Fix the return type too.One solution:
Solution
1functiongroupBy<T,K>(2 items:readonlyT[],3keyOf:(item:T)=>K4):Map<K,T[]>{5const groups =newMap<K,T[]>()6for(const item of items){7const key =keyOf(item)8const group = groups.get(key)9if(group) group.push(item)10else groups.set(key,[item])11}12return groups13}
K is inferred from what the callback returns, here 'fiction' | 'poetry', so the map's keys are the genres, not any string. groups.get(key) is T[] | undefined, and checking it is what lets you push. A Map keeps keys of any type and has no inherited keys, which a plain object does not promise. The readonly T[] parameter accepts readonly and ordinary arrays alike, and promises the helper will not change the list. (Generics; soundness.)
20. Mastery challenge · Write it: links that know their parameters · Code challengeWrite Params<Path>, the union of the :name parts of a path, and make link(path, params) replace each :name with params[name]. link('/books/:bookId', { bookId: '7' }) is '/books/7'.One solution:
Params takes the first :name before a slash and recurses on the rest, so the union collects every name; Record<Params<P>, string> then requires each one as a key. Inside link, P is still generic and the checker cannot index params with a string it found at run time, so assigning it to a plain Record<string, string> (safe: every value is a string) lets the replacer look names up. With Params left as string, any object is accepted, missing keys included, which is what the last line tests. (Template literal types; generics.)