Skip to the content

Module 1: Your first program

Module 2: Values and variables

Module 3: Decisions and loops

Module 4: Functions

Module 5: Arrays, objects and missing values

Module 6: Modules, errors and waiting

Module 7: Meet TypeScript

Module 8: Everyday types

Module 9: Unions and narrowing

Module 10: Shapes, classes and escape hatches

Module 11: Typing async code, data and errors

Module 12: Generics

Module 13: Types from values

Module 14: Utility types and mapped types

Module 15: Conditional types and infer

Module 16: Template literal types

Module 17: Soundness, brands and type tests

Module 18: tsconfig, modules and declarations

Module 19: Type-level programming

Module 20: Mastery challenge

Answer key

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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
    const first = 'Ada'const last = 'Lovelace'const books = 3
    const fullName = `${first} ${last}`const 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.
  9. 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.
  10. 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.
  11. 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.
  12. 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.
  13. 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).
  14. 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.
  15. 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.
  16. 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.
  17. 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.
  18. 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.
  19. 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.
  20. 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).
  21. 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:
    Solution
    function lateFee(daysLate, perDay = 2) {  if (daysLate <= 0) {    return 0  }  return daysLate * perDay}
    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.
  22. 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.
  23. 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.
  24. 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.
  25. 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.
  26. 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
    function lateFees(days) {  return days.filter((d) => d > 0).map((d) => d * 2)}
    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.
  27. 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.
  28. 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
    function cartTotal(cart) {  let total = 0  for (const line of cart) {    total += line.price * line.quantity  }  return total}
    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.
  29. 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.
  30. 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.
  31. 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.
  32. 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.
  33. 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.
  34. 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.
  35. 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.
  36. 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:
    Solution
    export async function countBooks(load) {  try {    const titles = await load()    return titles.length  } catch (error) {    return 0  }}
    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.
  37. 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.
  38. 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".
  39. 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.
  40. 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:
    Solution
    function lateFee(daysLate: number): number {  return daysLate * 2}
    const fee = lateFee(4)
    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.
  41. 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.
  42. 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 or undefined, 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.
  43. 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].
  44. 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.
  45. 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.
  46. 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.
  47. 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.
  48. 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
    interface Book {  title: string  author: string  subtitle?: string}
    function label(book: Book): string {  const name = book.subtitle    ? `${book.title}: ${book.subtitle}`    : book.title  return `${name} by ${book.author}`}
    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.
  49. 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.
  50. 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.
  51. 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.
  52. 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.
  53. 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:
    Solution
    type Loan =  | { status: 'active'; daysLate: number }  | { status: 'returned' }  | { status: 'lost'; price: number }
    function fee(loan: Loan): number {  switch (loan.status) {    case 'active':      return loan.daysLate * 2    case 'returned':      return 0    case 'lost':      return loan.price + 5  }}
    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.
  54. 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.
  55. 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.
  56. 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:
    Solution
    class Shelf {  readonly capacity: number  private books: string[] = []
      constructor(capacity: number) {    this.capacity = capacity  }
      add(title: string): boolean {    if (this.books.length >= this.capacity) return false    this.books.push(title)    return true  }
      get count() {    return this.books.length  }}
    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.
  57. 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.
  58. 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.
  59. 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.
  60. 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.)
  61. 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.
  62. 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.
  63. 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.
  64. 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
    type Result<T> =  | { ok: true; value: T }  | { ok: false; error: string }
    function parsePages(text: string): Result<number> {  const pages = Number(text)  if (Number.isNaN(pages)) return { ok: false, error: 'Not a number' }  if (pages <= 0) return { ok: false, error: 'Must be more than 0' }  return { ok: true, value: pages }}
    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.
  65. 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].
  66. 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.)
  67. 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.
  68. 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
    function indexBy<T, K extends keyof T>(  items: T[],  key: K): Map<T[K], T> {  const index = new Map<T[K], T>()  for (const item of items) index.set(item[key], item)  return index}
    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.
  69. 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.
  70. 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.
  71. 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.
  72. 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.
  73. 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
    const GENRES = ['fiction', 'poetry', 'history'] as consttype Genre = (typeof GENRES)[number]
    function isGenre(value: string): value is Genre {  return GENRES.some((genre) => genre === value)}
    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.
  74. 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.
  75. 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.
  76. 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.
  77. 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.
  78. 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.
  79. 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.
  80. 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:
    Solution
    type Editable<T> = {  -readonly [K in keyof T as Exclude<K, 'id'>]: T[K]}
    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.
  81. 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.
  82. 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.
  83. 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).
  84. 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.
  85. 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.
  86. 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:
    Solution
    type Unwrap<T> =  T extends readonly (infer E)[] ? Unwrap<E>  : T extends Promise<infer V> ? Unwrap<V>  : T
    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.
  87. 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.
  88. 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.
  89. 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.
  90. 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
    type GetterName<K extends string> = `get${Capitalize<K>}`
    function getterName<K extends string>(key: K): GetterName<K> {  const first = key.charAt(0).toUpperCase()  const name = `get${first}${key.slice(1)}`  return name as GetterName<K>}
    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.
  91. 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.
  92. 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.
  93. 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.
  94. 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.
  95. 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.
  96. 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.
  97. 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.
  98. 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
    type Isbn = string & { readonly __brand: 'Isbn' }
    function isbn(text: string): Isbn {  if (!/^\d{13}$/.test(text)) {    throw new Error(`Not an ISBN: ${text}`)  }  return text as Isbn}
    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.
  99. 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 ??.
  100. 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.
  101. 18. tsconfig, modules and declarations · What strict turns on · QuizWhich of these does strict: true not 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.
  102. 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.
  103. 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.
  104. 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.)
  105. 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.
  106. 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.
  107. 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.
  108. 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 ''.
  109. 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:
    Solution
    type PathValue<T, P extends string> =  P extends `${infer Head}.${infer Rest}`    ? Head extends keyof T      ? PathValue<T[Head], Rest>      : never    : P extends keyof T      ? T[P]      : never
    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.
  110. 19. Type-level programming · const type parameters infer literals · QuizWhy is the constraint on exactShelves readonly 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.
  111. 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.
  112. 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.)
  113. 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.)
  114. 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.)
  115. 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.
  116. 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).
  117. 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.
  118. 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).
  119. 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.
  120. 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:
    Solution
    type Loan =  | { status: 'requested'; bookId: number }  | { status: 'active'; bookId: number; due: string }  | { status: 'returned'; bookId: number }
    type LoanEvent = { type: 'approve'; due: string } | { type: 'return' }
    function next(loan: Loan, event: LoanEvent): Loan {  const { bookId } = loan  if (loan.status === 'requested' && event.type === 'approve') {    return { status: 'active', bookId, due: event.due }  }  if (loan.status === 'active' && event.type === 'return') {    return { status: 'returned', bookId }  }  throw new Error(`Cannot ${event.type} a ${loan.status} loan`)}
    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.)
  121. 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
    function groupBy<T, K>(  items: readonly T[],  keyOf: (item: T) => K): Map<K, T[]> {  const groups = new Map<K, T[]>()  for (const item of items) {    const key = keyOf(item)    const group = groups.get(key)    if (group) group.push(item)    else groups.set(key, [item])  }  return groups}
    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.)
  122. 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:
    Solution
    type Params<Path extends string> =  Path extends `${string}:${infer Name}/${infer Rest}`    ? Name | Params<`/${Rest}`>    : Path extends `${string}:${infer Name}`      ? Name      : never
    function link<P extends string>(  path: P,  params: Record<Params<P>, string>): string {  const values: Record<string, string> = params  return path.replace(/:(\w+)/g, (_, name: string) => values[name])}
    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.)