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.
- 01Write 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. - 02Read library signaturesRead declarations like
map<U>(…): U[]andquerySelector<E extends Element = Element>, and predict the type a call gives back. - 03Keep 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:
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:
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 noprise, andpriceis 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.
Product[] in the angle brackets. TypeScript fills in T with it, so the helper gives back a Promise<Product[]>. Nothing looks inside the file.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
fetchJsonloads 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
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:
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:
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();Tea costs $3Product[]No errorsT 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.
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:
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:
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:
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:Kmust be one of the event names, here"add"or"remove"(the Generic constraints lesson coverskeyof).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.
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:
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 });Added Tea for $3No errors"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.
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:
map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];Read it left to right:
map<U>:maphas a type parameter,U.Tisn't declared here: it belongs to the array, and is the type of its items.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 aU.thisArg?: any: an optional second parameter you'll rarely use.: U[]:mapgives back a list of whatever your callback gives back.
So the callback decides U. Check it on a call:
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 ].
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:
querySelector<K extends keyof HTMLElementTagNameMap>(selectors: K): HTMLElementTagNameMap[K] | null;
querySelector<E extends Element = Element>(selectors: string): E | null;- Line 1 is used when you pass a tag name.
HTMLElementTagNameMapis a type that lists every HTML tag with its element type, likeinput: HTMLInputElement.K extends keyof HTMLElementTagNameMapmeansKmust be one of those tag names, and you get back that tag's element type, ornullwhen nothing matches. - Line 2 takes any other
string.E extends ElementmeansEmust be some kind of element. It also has a default,= Element: when nothing fills inE, it'sElement, the most general element type.
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:
const found = document.querySelector("input");// ^?"input"HTMLInputElement | null"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.
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:
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:
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:
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.
| When you write | What goes wrong | Write instead |
|---|---|---|
function logValue<T>(value: T): void | T 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 check | T 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:
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:
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.
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.
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");
Sort each line by what TypeScript says about it. Each line is checked on its own.
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.
| The call | Who 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 theUserexample printedundefinedwith no error. - "I have to write the type argument on every call." No. TypeScript fills it in from the arguments, as in
cart.map(…)andfetchChecked(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
anyinside the emitter makes every call unchecked." No. Callers useonandemit, whose types are exact. Theanyis 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.
- "
querySelectorreads my CSS selector." No. It only knows whether the whole text is a tag name."#quantity"and"form input"both giveElement.
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.
The emitter's methods are left empty here, because only their types matter. Three events are sent. Which line will TypeScript mark?
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" });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" }); // line 14: the price is textFor "add", the payload's price must be a number. Line 14 gives the text "4", so TypeScript reports Type 'string' is not assignable to type 'number' there.
What type does TypeScript give prices? Guess first. Then point at prices, or press Edit & run and add a // ^? line under it.
const prices = ["3", "4"].map((text) => Number(text));console.log(prices);const prices = ["3", "4"].map((text) => Number(text));// ^?console.log(prices);Number(text) gives back a number, so U is number and map gives back number[]. The query on line 2 shows const prices: number[], and the code prints [ 3, 4 ].
TypeScript marks line 11. Fix it with a type argument. What does the fixed code print?
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();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<Product[]>("products.json"); // ^? const cake = products[1]; console.log(`${cake.name} costs $${cake.price}`);}showCake();With <Product[]>, products is a Product[], so products[1] is a product and line 12 may read its name and price. The second product in the file is the cake, so it prints Cake costs $4.
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?
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 });type CartEvents = { add: { name: string; price: number }; remove: { name: string }; checkout: { total: number };}; 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 });Adding checkout: { total: number } to the event map makes "checkout" a real event name and gives its payload a total. Both calls are now fine, and the listener prints Paid $7. The class itself didn't change.
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?
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);function filterBy<T>(items: T[], test: (item: T) => boolean): T[] { return items.filter(test);} const short = filterBy<string>(["Tea", "Cake", "Bun"], (name: string) => name.length === 3);console.log(short);F appeared once, for test, so it connected nothing. With only T, filterBy<string> is a complete set of type arguments, the error is gone, and the code prints [ 'Tea', 'Bun' ].
TypeScript reports no errors. What does the starter print? Then change it so the quantities are checked and turned into numbers as they arrive.
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();async function fetchChecked<T>(url: string, check: (data: unknown) => T): Promise<T> { const response = await fetch(url); return check(await response.json());} function toNumbers(data: unknown): number[] { if (!Array.isArray(data)) throw new Error("Expected a list"); return data.map((item) => Number(item));} async function showItems() { const quantities = await fetchChecked("quantities.json", toNumbers); console.log(quantities[0] + quantities[1]);}showItems();The starter prints 21: the quantities are text, so + joins them. fetchChecked with toNumbers turns each item into a number while the page runs, TypeScript fills in T as number[] from toNumbers, and the code prints 3.
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.
Question 1 of 7What does the type argument in
fetchJson<Product[]>("products.json")do?Choose an answer to see the explanation.
Question 2 of 7No type argument is written here. What type does
productsget?Read the code, then predictasync 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.
Question 3 of 7Which line of this cart emitter does TypeScript mark?
Read the code, then predicttype 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.
Question 4 of 7Array's
mapis declared asmap<U>(callbackfn: (value: T, …) => U): U[]. What type islong?Read the code, then predictconst cart = ["Tea", "Cake"]; const long = cart.map((name) => name.length > 3);Choose an answer to see the explanation.
Question 5 of 7What type does TypeScript give
totalhere?Read the code, then predictconst total = document.querySelector(".total");Choose an answer to see the explanation.
Question 6 of 7Which function has a type parameter it doesn't need?
Choose an answer to see the explanation.
Question 7 of 7This reads products.json as a
User. Does TypeScript report an error, and what prints?Read the code, then predicttype 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 ofany, but the type argument is a claim you make: nothing checks the file.- A typed event emitter uses an event map,
K extends keyof Eventsfor the name, andEvents[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 = Elementis used when nothing fills inE. - A type parameter should connect two things. If it appears once, use
unknownor 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.