cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

Generics in practice

Use TypeScript generics in real frontend code: a typed fetchJson<T>, a typed event emitter, reading library signatures, and keeping type parameters simple.

By the end, you can
  • 01
    Write typed helpersBuild a fetchJson<T> and a typed event emitter, and say which of their checks are real and which take your word for it.
  • 02
    Read library signaturesRead declarations like map<U>(…): U[] and querySelector<E extends Element = Element>, and predict the type a call gives back.
  • 03
    Keep generics simpleSpot a type parameter that connects nothing, drop extra ones, and check outside data instead of only naming its type.

One fetch helper for every kind of data

A shop page loads its menu from a file on the server, products.json, which holds a tea at 3 and a cake at 4. The browser's fetch asks for the file, and response.json() turns its text into objects (the fetch and async/await lessons cover both). Here is what TypeScript knows about the result:

A menu loaded with plain fetchPop out in the code editor (opens in a new tab)TypeScript
async function showMenu() {  const response = await fetch("products.json");  const products = await response.json();  //    ^?  console.log(products[0].prise);}showMenu();

Line 2 asks for the file, and line 3 turns it into objects. The query on line 4 shows const products: any. any means "could be anything, so don't check it" (see any, unknown & never). So line 5 misspells price as prise, TypeScript says nothing, and the code prints undefined.

Every page that loads data has this problem. You could write a type on every fetch call, or write one helper that takes the type as a type argument, the type you put in angle brackets when you call a generic function:

A typed fetch helper (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type Product = { name: string; price: number }; async function fetchJson<T>(url: string): Promise<T> {  const response = await fetch(url);  return response.json();} async function showMenu() {  const products = await fetchJson<Product[]>("products.json");  //    ^?  console.log(products[0].prise);}showMenu();

Line 1 says what a product is. Lines 3 to 6 are the helper, fetchJson. Its type parameter T stands for "whatever the file holds", and it gives back a Promise<T>, a promise that finishes with a T. Line 9 calls it with <Product[]>, and the query on line 10 shows const products: Product[]. Now line 11 gets an error, on purpose:

  • Property 'prise' does not exist on type 'Product'. Did you mean 'price'? means a product has no prise, and price is the closest name it has.

Run it anyway and it still prints undefined: the types are removed before the code runs. But you would have fixed the typo before anyone saw it.

A card reads fetchJson<Product[]>, labelled you write the type. An arrow leads to a tag reading T is Product[], and another arrow to a card reading Promise<Product[]>, labelled what you get back. A note says TypeScript never opens products.json.fetchJson<Product[]>you write the typeT isProduct[]Promise<Product[]>what you get backTypeScript never opens products.json
You write Product[] in the angle brackets. TypeScript fills in T with it, so the helper gives back a Promise<Product[]>. Nothing looks inside the file.
Real-life analogyA takeaway order with your name on the bag

At a takeaway counter, one kitchen makes every order. You say what you want, and the bag comes back with your order written on it, so everyone who carries it knows what's inside. The label is copied from what you asked for. Nobody opens the bag to compare.

In real life: One counter makes any order
In TypeScript: One fetchJson loads any data
In real life: You say "two teas" when you order
In TypeScript: You write <Product[]> when you call
In real life: The bag is labelled "two teas"
In TypeScript: The result is typed Product[]
In real life: Nobody opens the bag to check the label
In TypeScript: TypeScript never looks inside products.json
The main idea

In real code, generics let one helper work for many types while the caller keeps an exact type: a fetch helper for any data, an event emitter for any events, a map for any list. A type parameter connects what goes in with what comes out. When nothing goes in, like a file's contents, the type you write is a claim you make, not a check.

A typed fetchJson<T>

Why does fetch give back any in the first place? Point a query at each step:

Where the any comes fromPop out in the code editor (opens in a new tab)TypeScript
async function loadMenu() {  const response = await fetch("products.json");  //    ^?  const data = response.json();  //    ^?  return data;}loadMenu().then((data) => console.log(data.length));

The query on line 3 shows const response: Response: TypeScript knows fetch gives back a response. The query on line 5 shows const data: Promise<any>. The browser can't know what a file holds before it arrives, so TypeScript's own description of json() says it gives back a promise of any. Line 8 prints 2, the number of products.

Now read fetchJson from the overview line by line. async function fetchJson<T>(url: string) declares the type parameter T and one ordinary parameter, the address. : Promise<T> says what it gives back. Inside, return response.json() hands over an any, and TypeScript allows any wherever a T goes. So the helper doesn't make the data safer. It moves the any into one place, and every caller gets a named type instead.

Press the buttons to change the type argument on line 9, and watch the query on line 10 and the error list:

Playground: choose the type argument
A menu loaded with fetchJsonPop out in the code editor (opens in a new tab)TypeScript
type Product = { name: string; price: number }; async function fetchJson<T>(url: string): Promise<T> {  const response = await fetch(url);  return response.json();} async function showMenu() {  const products = await fetchJson<Product[]>("products.json");  //    ^?  const first = products[0];  console.log(`${first.name} costs $${first.price}`);}showMenu();
Console outputTea costs $3
What happens
productsProduct[]
TypeScriptNo errors
Try it yourself

T is Product[], so fetchJson gives back a Promise<Product[]>, and after await, products is a Product[]. Line 11 takes the first product, and line 12 reads its name and price. No errors, and it prints Tea costs $3.

TypeScript checks the code for real each time, and the output is what this code prints. Each button changes only line 9. Reset goes back to Product[].

Product[] is the right answer, and line 12 is fine. Product says "one product", so products[0] on line 11 makes no sense to TypeScript. No type argument gives unknown: T appears in no parameter, so TypeScript has nothing to fill it in from. any switches the checks off again. Look at the console output: it's Tea costs $3 every time. The type argument changes what TypeScript checks, never what the code does.

That cuts both ways. Here the helper is asked for a User while loading the products file:

The wrong type argument, and no errorPop out in the code editor (opens in a new tab)TypeScript
type User = { name: string; age: number }; async function fetchJson<T>(url: string): Promise<T> {  const response = await fetch(url);  return response.json();} async function showUser() {  const user = await fetchJson<User>("products.json");  //    ^?  console.log(user.age);}showUser();

Line 1 describes a user with a name and an age. Line 9 loads products.json but says it holds a User. The query on line 10 shows const user: User, and there are no errors. Line 11 prints undefined, because the file holds a list of products, and a list has no age.

T appears only in the result, Promise<T>, so no argument can disagree with it. A type parameter like that works the same way as as (the Type assertions lesson covers it): you tell TypeScript what the value is, and it believes you. That's fine for data you trust, like your own server. The practical section shows how to check data you don't.

A typed event emitter

A cart sends messages when things happen: "tea was added", "cake was removed". Other parts of the page listen: the badge with the item count, the total, a saved copy. An event emitter lets code listen with on(name, handler) and send with emit(name, payload), where the payload is the data that goes with the message (the Build an event emitter lesson builds one in JavaScript). Here is one typed only with string and any:

An emitter that checks nothingPop out in the code editor (opens in a new tab)TypeScript
class Emitter {  private listeners: { name: string; run: (payload: any) => void }[] = [];   on(name: string, handler: (payload: any) => void) {    this.listeners.push({ name, run: handler });  }   emit(name: string, payload: any) {    for (const listener of this.listeners) {      if (listener.name === name) listener.run(payload);    }  }} const cart = new Emitter();cart.on("add", (item) => console.log(`Added ${item.name} for $${item.price}`));cart.emit("add", { name: "Tea", price: 3 });cart.emit("add", { name: "Cake" });cart.emit("ad", { name: "Bun", price: 2 });

Line 2 keeps a list of listeners: an event name and the function to run. Line 4's on adds one, and line 8's emit runs every listener with that name. Line 16 listens for "add". Line 17 prints Added Tea for $3. Line 18 forgets the price and prints Added Cake for $undefined. Line 19 misspells the event as "ad", so no listener runs, and nothing prints. TypeScript reports nothing, because any name is a string and any payload is any.

The fix is one type that lists every event and its payload, and a type parameter for the event name. The class takes that list as its own type parameter, Events:

An emitter that knows its eventsPop out in the code editor (opens in a new tab)TypeScript
type CartEvents = {  add: { name: string; price: number };  remove: { name: string };}; class Emitter<Events> {  private listeners: { name: keyof Events; run: (payload: any) => void }[] = [];   on<K extends keyof Events>(name: K, handler: (payload: Events[K]) => void) {    this.listeners.push({ name, run: handler });  }   emit<K extends keyof Events>(name: K, payload: Events[K]) {    for (const listener of this.listeners) {      if (listener.name === name) listener.run(payload);    }  }} const cart = new Emitter<CartEvents>();cart.on("add", (item) => console.log(`Added ${item.name} for $${item.price}`));//              ^?cart.on("remove", (item) => console.log(`Removed ${item.name}`));cart.emit("add", { name: "Tea", price: 3 });

Lines 1 to 4 are the event map, CartEvents: each property name is an event, and its type is that event's payload. Line 6 makes the class generic, Emitter<Events>, so the same class works for a cart, a form, or a player. Line 9 is the key line:

  • K extends keyof Events: K must be one of the event names, here "add" or "remove" (the Generic constraints lesson covers keyof).
  • Events[K]: the type of that property, the payload for that one event. For "add", it's { name: string; price: number }.

Line 13 does the same for emit. Line 7 stores every handler with a payload: any, because one list holds handlers for different events. That any stays inside the class, in a private field (see Access modifiers): code outside only uses on and emit, and those are checked. Line 20 writes the type argument, new Emitter<CartEvents>(), because there's no argument to fill Events in from. The query on line 22 shows that item has a name and a price, and you never wrote its type. Line 24 prints Added Tea for $3.

A tag reads on("add", handler). An arrow leads into a window titled CartEvents, which holds two cards: add: { name, price } and remove: { name }. An arrow from the add card leads to a card reading { name, price }, labelled the handler's item.on("add",handler)K is "add"CartEventsadd: { name, price }remove: { name }Events[K] picks the payload{ name, price }the handler's item
on("add", …) makes K the name "add". Events[K] looks that name up in CartEvents, so the handler's item has a name and a price.

Now send different events on line 24:

Playground: emit a cart event
A cart emitter with one event sentPop out in the code editor (opens in a new tab)TypeScript
type CartEvents = {  add: { name: string; price: number };  remove: { name: string };}; class Emitter<Events> {  private listeners: { name: keyof Events; run: (payload: any) => void }[] = [];   on<K extends keyof Events>(name: K, handler: (payload: Events[K]) => void) {    this.listeners.push({ name, run: handler });  }   emit<K extends keyof Events>(name: K, payload: Events[K]) {    for (const listener of this.listeners) {      if (listener.name === name) listener.run(payload);    }  }} const cart = new Emitter<CartEvents>();cart.on("add", (item) => console.log(`Added ${item.name} for $${item.price}`));//              ^?cart.on("remove", (item) => console.log(`Removed ${item.name}`));cart.emit("add", { name: "Tea", price: 3 });
Console outputAdded Tea for $3
What happens
TypeScriptNo errors
Try it yourself

"add" is one of the event names, and its payload has a name and a price, as CartEvents says. No errors, and the add listener prints Added Tea for $3.

TypeScript checks the code for real each time, and the output is what this code prints. Each button changes only line 24. Reset goes back to Add tea.

The two mistakes from the loose emitter are now errors: Add without a price and Misspell the event. Remove with a price gets an excess property error, because an object written right in the call may only have the properties its type lists (see Excess property checks). In every case the console shows what plain JavaScript would do: a wrong payload still reaches the listener, and a misspelled event is silently lost.

Reading library type signatures

TypeScript describes the browser and JavaScript's built-ins in its own library files, and they're full of generics. When you hover a method in your editor, you see its signature: its name, type parameters, parameters, and what it gives back. Here is map, as TypeScript's lib.es5.d.ts declares it for an array of T:

Array's map, as TypeScript's library declares itText
map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];

Read it left to right:

  1. map<U>: map has a type parameter, U. T isn't declared here: it belongs to the array, and is the type of its items.
  2. callbackfn: (value: T, index: number, array: T[]) => U: the first parameter is your callback. It gets an item, its position, and the whole array, and gives back a U.
  3. thisArg?: any: an optional second parameter you'll rarely use.
  4. : U[]: map gives back a list of whatever your callback gives back.

So the callback decides U. Check it on a call:

map with U filled inPop out in the code editor (opens in a new tab)TypeScript
const cart = ["Tea", "Cake"];const lengths = cart.map((name) => name.length);//                   ^?//    ^?console.log(lengths);

Line 1 makes a cart of names. Line 2 maps each name to its length. The query on line 3 points at map, and shows the signature with everything filled in: T is string, and U is number, because name.length is a number. The query on line 4 asks about lengths on the same line: const lengths: number[]. Line 5 prints [ 3, 4 ].

Three cards split map's signature: map<U>, labelled a type to fill in; (callbackfn: (value: T) => U), labelled U is what your callback gives back; and : U[], labelled a list of U. Under them, for cart.map((name) => name.length): U is number, the callback gives a number, and the result is number[].map<U>a type to fill in(callbackfn: (value: T) => U)U is what your callback gives back: U[]a list of UU = number(name) => name.lengthnumber[]
Read a signature left to right: the type parameters, the parameters, then what you get back. In cart.map((name) => name.length), the callback gives back a number, so U is number.

document.querySelector, which finds the first element matching a CSS selector (see Walking & searching the DOM), has several declarations, called overloads. These two from lib.dom.d.ts matter most:

Two of querySelector's declarations, from TypeScript's libraryText
querySelector<K extends keyof HTMLElementTagNameMap>(selectors: K): HTMLElementTagNameMap[K] | null;
querySelector<E extends Element = Element>(selectors: string): E | null;
  1. Line 1 is used when you pass a tag name. HTMLElementTagNameMap is a type that lists every HTML tag with its element type, like input: HTMLInputElement. K extends keyof HTMLElementTagNameMap means K must be one of those tag names, and you get back that tag's element type, or null when nothing matches.
  2. Line 2 takes any other string. E extends Element means E must be some kind of element. It also has a default, = Element: when nothing fills in E, it's Element, the most general element type.
What three querySelector calls give backPop out in the code editor (opens in a new tab)TypeScript
const search = document.querySelector("input");//    ^?const total = document.querySelector(".total");//    ^?const quantity = document.querySelector<HTMLInputElement>("#quantity");//    ^?console.log(quantity?.value);

Line 1 passes the tag name "input", and line 2 shows HTMLInputElement | null. Line 3 passes a class, ".total", which isn't a tag name, so line 4 shows Element | null. Line 5 writes the type argument itself, <HTMLInputElement>, and line 6 shows HTMLInputElement | null. Line 7 prints 2, the value in the page's quantity box. Notice that E appears only in the result, just like T in fetchJson: TypeScript takes your word that #quantity is an input.

Type your own selector, and watch the answer after ^? on line 2:

Playground: what querySelector gives back
One querySelector callPop out in the code editor (opens in a new tab)TypeScript
const found = document.querySelector("input");//    ^?
Selector"input"
What TypeScript gives back
foundHTMLInputElement | null
Try it yourself

"input" is a tag name, so the first declaration matches, and HTMLElementTagNameMap["input"] is HTMLInputElement. | null is there because the page might have no input.

The code is checked by the real TypeScript compiler each time you type. Only the text inside the quotes changes. Reset puts input back.

Tag names like button and img give their own element types. #quantity and form input give Element: TypeScript doesn't read CSS, it only checks whether the whole text is a tag name.

Keeping generics simple

Generics are easy to overdo. A good type parameter connects two things: an argument and the result, or two arguments. A type parameter that appears only once connects nothing:

A type parameter that connects nothingPop out in the code editor (opens in a new tab)TypeScript
function logValue<T>(value: T): void {  console.log(value);} function logPlain(value: unknown): void {  console.log(value);} logValue("Tea");logPlain("Tea");

Line 1 declares T and uses it once, for value. Line 5 does the same job with value: unknown. Both accept anything, both print Tea on lines 9 and 10, and neither can do more with the value. The second is simpler to read. When you see a type parameter used only once, write unknown or the type you need instead.

Extra type parameters also make a function harder to call:

A type parameter too many (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
function filterBy<T, F extends (item: T) => boolean>(items: T[], test: F): T[] {  return items.filter(test);} console.log(filterBy([3, 4, 12], (price) => price < 5));console.log(filterBy<number>([3, 4, 12], (price: number) => price < 5));

Line 1 gives filterBy two type parameters: T for an item and F for the test function. Line 5 lets TypeScript fill in both, and prints [ 3, 4 ]. Line 6 writes the item type itself, and gets an error on purpose: Expected 2 type arguments, but got 1, meaning "if you write the type arguments, write them all". F is used once, so it only gets in the way:

One type parameter is enoughPop out in the code editor (opens in a new tab)TypeScript
function filterBy<T>(items: T[], test: (item: T) => boolean): T[] {  return items.filter(test);} console.log(filterBy([3, 4, 12], (price) => price < 5));console.log(filterBy<number>([3, 4, 12], (price) => price < 5));

Line 1 types the test directly: test: (item: T) => boolean. Line 5 and line 6 now both work, and both print [ 3, 4 ].

The table below sums up the habits. Read each row as: what you wrote, what goes wrong, and what to write instead.

Generics that ask for too much, and simpler versions
When you writeWhat goes wrongWrite instead
function logValue<T>(value: T): voidT appears once, so it connects nothing. It only makes the function harder to read.function logValue(value: unknown): void.
filterBy<T, F extends (item: T) => boolean>F is used once. Anyone who writes the type arguments must write both, or gets Expected 2 type arguments, but got 1.filterBy<T>(items: T[], test: (item: T) => boolean).
fetchJson<T>(url): Promise<T> for data you could checkT only appears in the result, so TypeScript takes your word for it, the way it does with as.fetchChecked(url, check), so T comes from a function that really checks the data.

A good way to start: write the function for one type first, like number[]. Add a type parameter only when a second caller needs a different type, and only where it connects an input with an output.

Prices checked as they arrive

A price list comes from another team's server, prices.json, and it sends the prices as text: ["3", "4"]. With fetchJson, you name the type you hope for:

Prices taken on trustPop out in the code editor (opens in a new tab)TypeScript
async function fetchJson<T>(url: string): Promise<T> {  const response = await fetch(url);  return response.json();} async function showTotal() {  const prices = await fetchJson<number[]>("prices.json");  console.log(prices[0] + prices[1]);}showTotal();

Line 7 says the file holds number[]. There are no errors, but line 8 prints 34: the prices are really text, and + joins text. A better helper takes a second argument, a function that checks the data and gives back the right type:

Prices checked as they arrivePop out in the code editor (opens in a new tab)TypeScript
async function fetchChecked<T>(url: string, check: (data: unknown) => T): Promise<T> {  const response = await fetch(url);  return check(await response.json());} function toPrices(data: unknown): number[] {  if (!Array.isArray(data)) throw new Error("Expected a list of prices");  return data.map((price) => Number(price));} async function showTotal() {  const prices = await fetchChecked("prices.json", toPrices);  //    ^?  console.log(prices[0] + prices[1]);}showTotal();

Line 1 is fetchChecked. Its check parameter is a function that takes the raw data, as unknown, and gives back a T. Line 3 runs it on whatever arrived. Lines 6 to 9 are the check for this file: line 7 stops with an error unless the data is a list (Array.isArray, see Checking types reliably), and line 8 turns every item into a number with Number. Line 12 passes toPrices and no type argument. The query on line 13 shows const prices: number[]: TypeScript filled in T from what toPrices gives back. Line 14 prints 7.

Now T appears twice, in the check and in the result, so it connects something real. The type comes from code that runs while the page runs, not from a claim you wrote.

A server window holds ["3", "4"]. One arrow leads to a card reading fetchJson<number[]>, then to a card reading "34", marked trusted. Another arrow leads to a card reading fetchChecked(toPrices), then to a card reading 7, marked checked.server["3", "4"]fetchJson<number[]>"34"trusted: "34"fetchChecked(toPrices)7checked: 7
The server sends the prices as text. fetchJson<number[]> only names the type, so the sum joins them into "34". fetchChecked runs toPrices on the data first, so the sum is 7.

Library types are full of generics like these. Sort each line by what TypeScript says about it. Each one uses a generic from TypeScript's library: querySelector, map, Map (see Map and Set), or Promise.

Error or fine?
  • const box = document.querySelector("input"); box?.value;
  • const box = document.querySelector(".quantity"); box?.value;
  • const box = document.querySelector<HTMLInputElement>(".quantity"); box?.value;
  • const sizes = ["Tea"].map((name) => name.length); sizes[0].toUpperCase();
  • const names = ["Tea"].map((name) => name.toUpperCase()); names[0].toLowerCase();
  • new Map<string, number>().set("tea", "3");
  • new Map<string, number>().set("tea", 3);
  • const total: Promise<number> = Promise.resolve("7");
Try it yourself
0 of 8 correct

Sort each line by what TypeScript says about it. Each line is checked on its own.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

The biggest mix-up is about who fills in a type parameter, and who checks the value. The table below compares the calls from this lesson. Read each row as: the call, where the type comes from, and who makes sure it's right.

Who fills in the type, and who checks it
The callWho fills in the type parameter?Who checks the data?
cart.map((name) => name.length)TypeScript, from what the callback gives back: U is number.TypeScript. It can see the callback's code.
new Emitter<CartEvents>()You. There's no argument to fill Events in from.TypeScript checks every on and emit call against CartEvents.
fetchJson<Product[]>(url)You. The address says nothing about what the file holds.Nobody. TypeScript takes your word for it.
document.querySelector<HTMLInputElement>("#quantity")You. The selector is only a string.Nobody. If #quantity isn't an input, nothing warns you.
fetchChecked(url, toPrices)TypeScript, from what toPrices gives back: number[].Your toPrices function, while the page runs.
  • "fetchJson<Product[]> checks that the file holds products." No. The type argument is a claim you make. Line 11 of the User example printed undefined with no error.
  • "I have to write the type argument on every call." No. TypeScript fills it in from the arguments, as in cart.map(…) and fetchChecked(url, toPrices). You write it only when no argument uses it.
  • "More type parameters make a function more flexible." No. A type parameter used once connects nothing, and makes callers who write type arguments write more of them.
  • "The any inside the emitter makes every call unchecked." No. Callers use on and emit, whose types are exact. The any is hidden inside the class.
  • "Library signatures are too hard to read." No. They follow one pattern: the name, the type parameters in angle brackets, the parameters, then what you get back.
  • "querySelector reads my CSS selector." No. It only knows whether the whole text is a tag name. "#quantity" and "form input" both give Element.

Practice exercises

Each exercise is TypeScript. When you change code, press Edit & run on the starter code: the errors and ^? answers update as you type, and the console shows what it prints. The data files from this lesson come along. Try a guess first, then use the hints.

Exercise 1 · Warm-upFind the event TypeScript marks

The emitter's methods are left empty here, because only their types matter. Three events are sent. Which line will TypeScript mark?

Starter codePop out in the code editor (opens in a new tab)TypeScript
type CartEvents = {
  add: { name: string; price: number };
  remove: { name: string };
};

class Emitter<Events> {
  on<K extends keyof Events>(name: K, handler: (payload: Events[K]) => void) {}
  emit<K extends keyof Events>(name: K, payload: Events[K]) {}
}

const cart = new Emitter<CartEvents>();
cart.emit("add", { name: "Tea", price: 3 });
cart.emit("remove", { name: "Tea" });
cart.emit("add", { name: "Cake", price: "4" });

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upRead what map gives back

    What type does TypeScript give prices? Guess first. Then point at prices, or press Edit & run and add a // ^? line under it.

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    const prices = ["3", "4"].map((text) => Number(text));console.log(prices);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeGive fetchJson its type argument

      TypeScript marks line 11. Fix it with a type argument. What does the fixed code print?

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      type Product = { name: string; price: number }; async function fetchJson<T>(url: string): Promise<T> {  const response = await fetch(url);  return response.json();} async function showCake() {  const products = await fetchJson("products.json");  //    ^?  const cake = products[1];  console.log(`${cake.name} costs $${cake.price}`);}showCake();

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeAdd a checkout event

        The page wants a "checkout" event with the order total, but TypeScript marks lines 21 and 22. Change only the event map so both lines work. What does the code print?

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        type CartEvents = {  add: { name: string; price: number };  remove: { name: string };}; class Emitter<Events> {  private listeners: { name: keyof Events; run: (payload: any) => void }[] = [];   on<K extends keyof Events>(name: K, handler: (payload: Events[K]) => void) {    this.listeners.push({ name, run: handler });  }   emit<K extends keyof Events>(name: K, payload: Events[K]) {    for (const listener of this.listeners) {      if (listener.name === name) listener.run(payload);    }  }} const cart = new Emitter<CartEvents>();cart.on("checkout", (order) => console.log(`Paid $${order.total}`));cart.emit("checkout", { total: 7 });

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeDrop the extra type parameter

          Line 5 writes the item type and gets Expected 2 type arguments, but got 1. Fix filterBy so line 5 works without changing it. How many type parameters are left?

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          function filterBy<T, F extends (item: T) => boolean>(items: T[], test: F): T[] {  return items.filter(test);} const short = filterBy<string>(["Tea", "Cake", "Bun"], (name: string) => name.length === 3);console.log(short);

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeCheck the quantities instead of trusting them

            TypeScript reports no errors. What does the starter print? Then change it so the quantities are checked and turned into numbers as they arrive.

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            async function fetchJson<T>(url: string): Promise<T> {  const response = await fetch(url);  return response.json();} async function showItems() {  const quantities = await fetchJson<number[]>("quantities.json");  console.log(quantities[0] + quantities[1]);}showItems();

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask where each type parameter gets its type: from an argument, or from what someone wrote. The quiz shows its code without the checker, so the error list doesn't give the answers away.

              The generics in practice quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does the type argument in fetchJson<Product[]>("products.json") do?

                Choose an answer to see the explanation.

              2. Question 2 of 7No type argument is written here. What type does products get?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                async function fetchJson<T>(url: string): Promise<T> {
                  const response = await fetch(url);
                  return response.json();
                }
                
                async function showMenu() {
                  const products = await fetchJson("products.json");
                }

                Choose an answer to see the explanation.

              3. Question 3 of 7Which line of this cart emitter does TypeScript mark?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type CartEvents = {
                  add: { name: string; price: number };
                  remove: { name: string };
                };
                
                class Emitter<Events> {
                  on<K extends keyof Events>(name: K, handler: (payload: Events[K]) => void) {}
                  emit<K extends keyof Events>(name: K, payload: Events[K]) {}
                }
                
                const cart = new Emitter<CartEvents>();
                cart.emit("remove", { name: "Tea" });
                cart.emit("add", { name: "Cake" });

                Choose an answer to see the explanation.

              4. Question 4 of 7Array's map is declared as map<U>(callbackfn: (value: T, …) => U): U[]. What type is long?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                const cart = ["Tea", "Cake"];
                const long = cart.map((name) => name.length > 3);

                Choose an answer to see the explanation.

              5. Question 5 of 7What type does TypeScript give total here?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                const total = document.querySelector(".total");

                Choose an answer to see the explanation.

              6. Question 6 of 7Which function has a type parameter it doesn't need?

                Choose an answer to see the explanation.

              7. Question 7 of 7This reads products.json as a User. Does TypeScript report an error, and what prints?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type User = { name: string; age: number };
                
                async function fetchJson<T>(url: string): Promise<T> {
                  const response = await fetch(url);
                  return response.json();
                }
                
                async function showUser() {
                  const user = await fetchJson<User>("products.json");
                  console.log(user.age);
                }
                showUser();

                Choose an answer to see the explanation.

              Key takeaways

              • fetchJson<T> gives every caller a named type instead of any, but the type argument is a claim you make: nothing checks the file.
              • A typed event emitter uses an event map, K extends keyof Events for the name, and Events[K] for the payload, so wrong names and payloads are errors.
              • Read a signature left to right: type parameters, parameters, result. The arguments you pass usually fill in the type parameters.
              • A default like E extends Element = Element is used when nothing fills in E.
              • A type parameter should connect two things. If it appears once, use unknown or a plain type.
              • For outside data, pass a check function, so the type comes from code that runs, not from a claim you write.

              Remember the one-liner.
              A type parameter earns its place when it connects what goes in with what comes out.

              Coming next: keyof & typeof. It looks closely at keyof, which this lesson used for event names, and at typeof, which turns a value into a type.

              CompleteFrontend Clear concepts. Working examples.