cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

Generic types & interfaces

Write TypeScript types that take a type parameter, like ApiResponse<T> and Field<T>, give them defaults, and read built-in ones like Map and Promise.

By the end, you can
  • 01
    Write a generic typeGive a type alias or an interface a type parameter, like ApiResponse<T>, and fill it in with ApiResponse<User>.
  • 02
    Use default type parametersWrite <T = string> so a type works without a type argument, and know when a written one wins.
  • 03
    Read built-in genericsRead Array<T>, Map<K, V>, Set<T>, and Promise<T>, including one generic inside another.

One reply type for every kind of data

A shop's server answers every request the same way: a status number, like 200 for "OK", and the data you asked for. Ask for products and data is a list of products. Ask for your profile and it's one user. Here are two types for those two replies:

Two reply types that differ in one placePop out in the code editor (opens in a new tab)TypeScript
type Product = { name: string; price: number };type User = { name: string; email: string }; type ProductsReply = { status: number; data: Product[] };type UserReply = { status: number; data: User }; const products: ProductsReply = { status: 200, data: [{ name: "Tea", price: 3 }] };const user: UserReply = { status: 200, data: { name: "Asha", email: "asha@example.com" } };console.log(products.data[0].name, user.data.name);

Lines 1 and 2 describe a product and a user with type, which gives a name to a type (the Type aliases and interfaces lesson covers it). Lines 4 and 5 describe the two replies. They are the same except for data: Product[] on line 4, User on line 5. Lines 7 and 8 make one reply of each, and line 9 prints Tea Asha.

A real shop has replies for orders, reviews, and coupons too. That's one more copy each time, and if the server adds a field, you change every copy. A generic type writes the reply once, with a blank where data's type goes:

One ApiResponse type for every replyPop out in the code editor (opens in a new tab)TypeScript
type Product = { name: string; price: number };type User = { name: string; email: string }; type ApiResponse<T> = { status: number; data: T }; const products: ApiResponse<Product[]> = { status: 200, data: [{ name: "Tea", price: 3 }] };const list = products.data;//    ^?const user: ApiResponse<User> = { status: 200, data: { name: "Asha", email: "asha@example.com" } };const profile = user.data;//    ^?console.log(list[0].name, profile.name);

Line 4 has the new part: <T> after the type's name. As in the Generic functions lesson, T is a type parameter, a name that stands for a type. Here it belongs to the type, and data: T says data has whatever type T is.

Line 6 writes ApiResponse<Product[]>. The type in the angle brackets is the type argument: it fills in T, so this reply's data is a Product[]. The query on line 8 shows const list: Product[]. Line 9 fills in T with User, and line 11 shows const profile: User. There are no errors, and line 12 prints Tea Asha.

On the left, the generic type ApiResponse<T>, whose data is T. Two arrows lead to ApiResponse<Product[]>, where T is Product[], and ApiResponse<User>, where T is User. TypeScript reads them as data: Product[] and data: User.the generic typewhat you writewhat TypeScript readsApiResponse<T>data: TApiResponse<Product[]>T = Product[]data: Product[]ApiResponse<User>T = Userdata: User
One generic type, used on lines 6 and 9. Each use fills in T: Product[] for the products, User for the user. TypeScript then reads data with that type.
Real-life analogyA printed label with a blank

A moving company prints one label for every box: "Contents: ____". Before a box goes on the truck, you write "books" or "plates" in the blank. Every label has the same layout, and the movers know what each box holds without opening it.

In real life: One printed label: "Contents: ____"
In TypeScript: One type: type ApiResponse<T> = { status: number; data: T }
In real life: You write "books" in the blank
In TypeScript: You write ApiResponse<Product[]>
In real life: Every label has the same layout
In TypeScript: Every ApiResponse has a status and a data
In real life: The movers know the box holds books without opening it
In TypeScript: TypeScript knows products.data is a Product[]
The main idea

A generic type is a type alias or an interface with a type parameter, like type ApiResponse<T> = { status: number; data: T }. Each time you use it, you fill in T with a type argument, like ApiResponse<User>, and TypeScript reads the type with T replaced.

Generic type aliases: a type with a blank

A type alias gets type parameters the same way: angle brackets after its name. A sign-up form is a good place for one. Every field has a value and maybe an error message, but the values differ: the email is text, the age is a number, and a checkbox is true or false.

One Field type for every form fieldPop out in the code editor (opens in a new tab)TypeScript
type Field<T> = { value: T; error: string | null }; const email: Field<string> = { value: "asha@example.com", error: null };const age: Field<number> = { value: 30, error: null };const nextAge = age.value + 1;//    ^?type AgeField = Field<number>;//   ^?console.log(email.value, nextAge);

Line 1 says a Field<T> has a value of type T and an error that is text or null (null here means "no error"). Line 3 makes an email field, Field<string>. Line 4 makes an age field, Field<number>, so age.value is a number. Line 5 adds 1 to it, and the query on line 6 shows const nextAge: number.

Line 7 gives Field<number> a name of its own. The query on line 8 shows what TypeScript reads: { value: number; error: string | null; }. It put number everywhere line 1 says T. Line 9 prints asha@example.com 31.

Unlike a function call, a generic type in an annotation doesn't work out T for you. Leave the angle brackets out and TypeScript stops you:

Field without a type argument (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type Field<T> = { value: T; error: string | null }; const name: Field = { value: "Asha", error: null };console.log(name.value);

Line 3 writes plain Field. TypeScript marks it, on purpose here: Generic type 'Field' requires 1 type argument(s) means "Field has one blank, and you didn't fill it in". The "(s)" is there because the same message is used for types with several blanks. Run it anyway and line 4 prints Asha: the types are gone by then.

Now try it yourself. Type a type argument for line 3 and watch the errors and the answer after ^? on line 5:

Playground: fill in the type argument
An email field with a type argumentPop out in the code editor (opens in a new tab)TypeScript
type Field<T> = { value: T; error: string | null }; const email: Field<string> = { value: "asha@example.com", error: null };const typed = email.value;//    ^?console.log(typed);
T is filled in withstring
What happens
value's typestring
TypeScriptNo errors
Try it yourself

Field<string> means value is text. The email is text, so there are no errors, and line 5 shows const typed: string.

The code is checked by the real TypeScript compiler each time you type. Only line 3 changes, and it always prints asha@example.com when it runs. Reset puts string back.

With string, the email fits. With number or boolean, the email on line 3 is the wrong kind of value, so TypeScript marks it, and line 5 still shows the type you asked for. string | null fits too, but then value may be null. Email isn't a type that exists, and empty brackets leave the blank unfilled.

An interface can only describe an object, but a type alias can name any type, so a generic alias can be a union, a type that is one of several:

Generic aliases that are unionsPop out in the code editor (opens in a new tab)TypeScript
type Maybe<T> = T | null;type OneOrMany<T> = T | T[]; type Tags = OneOrMany<string>;//   ^?function toList(tags: Tags): string[] {  return Array.isArray(tags) ? tags : [tags];} const coupon: Maybe<string> = null;console.log(toList("sale"), toList(["new", "sale"]), coupon);

Line 1 makes Maybe<T>: a T or null. Line 2 makes OneOrMany<T>: one T or a list of them, like a tag box that takes one tag or several. Line 4 fills it in with string, and the query on line 5 shows type Tags = string | string[].

Lines 6 to 8 turn either kind into a list: Array.isArray checks whether tags is already one. Line 10 makes a coupon code that is Maybe<string>, so null fits. Line 11 prints [ 'sale' ] [ 'new', 'sale' ] null.

Generic interfaces

Interfaces take type parameters too, written the same way. A product list that loads one page at a time is a common use:

A page of items as a generic interfacePop out in the code editor (opens in a new tab)TypeScript
interface Page<T> {  items: T[];  page: number;  hasMore: boolean;} type Product = { name: string; price: number }; const firstPage: Page<Product> = {  items: [{ name: "Tea", price: 3 }, { name: "Cake", price: 4 }],  page: 1,  hasMore: true,};const shown = firstPage.items;//    ^?console.log(shown.length, firstPage.hasMore);

Lines 1 to 5 declare Page<T>: a list of T items, the page number, and whether there are more pages. Line 9 says firstPage is a Page<Product>, so every item must be a product. Line 14 reads the items, and the query on line 15 shows const shown: Product[]. Line 16 prints 2 true.

Generic types and generic functions work well together. A function can take a Page<T> with its own type parameter, and TypeScript fills in that T from the page you pass:

A function that takes any Page (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
interface Page<T> {  items: T[];  page: number;  hasMore: boolean;} function nextPage<T>(current: Page<T>, items: T[]): Page<T> {  return { items, page: current.page + 1, hasMore: items.length > 0 };} const sizes: Page<string> = { items: ["S", "M"], page: 1, hasMore: true };const more = nextPage(sizes, ["L"]);//    ^?const broken = nextPage(sizes, [42]);console.log(more.page, more.items);

Line 7 declares nextPage<T>. It takes the current page and the new items, and gives back the next page with the same T. Line 11 makes a page of sizes, a Page<string>. Line 12 passes it with ["L"], so T is string, and line 13 shows const more: Page<string>.

Line 14 passes the same page with [42]. The page says T is string, so the new items must be text too. TypeScript marks the 42: Type 'number' is not assignable to type 'string' means "this list should hold text, and 42 is a number". Line 15 prints 2 [ 'L' ].

The table below compares the two ways to write a generic type. Read each row as a question, then each answer.

Generic type aliases and generic interfaces
QuestionGeneric type aliasGeneric interface
How do you write one?type Field<T> = { value: T; error: string | null };interface Field<T> { value: T; error: string | null }
How do you use it?Write the type argument in angle brackets: Field<string>.The same way: Field<string>.
Can it be a union, like T | null?Yes. An alias can name any type, like Maybe<T>.No. An interface always describes an object's shape.
Can its type parameter have a default?Yes: type Field<T = string>.Yes: interface Field<T = string>.

For an object shape, either works, and you use them the same way. Pick the one your project already uses.

Default type parameters

Most fields on a form hold text. Writing Field<string> every time is repetitive. A default type parameter says which type to use when you leave the type argument out. You write it with =, like a default value for a function parameter:

A Field whose T is string by defaultPop out in the code editor (opens in a new tab)TypeScript
type Field<T = string> = { value: T; error: string | null }; const name: Field = { value: "Asha", error: null };//    ^?const age: Field<number> = { value: 30, error: null };//    ^?console.log(name.value, age.value);

Line 1 says T = string: when nothing fills in T, use string. Line 3 writes plain Field, and the query on line 4 shows const name: Field<string>. Line 5 writes Field<number>, and line 6 shows Field<number>: a type argument you write wins over the default. Line 7 prints Asha 30.

Left: const name: Field, with no type argument. The default fills in T as string. Right: const age: Field<number>. The type argument you wrote fills in T as number.No type argumentconst name: FieldT = stringthe default fills it inType argument writtenconst age: Field<number>T = numberthe type you wrote wins
type Field<T = string> gives T a default. Line 3 writes plain Field, so the default fills in string. Line 5 writes Field<number>, and the type argument you wrote wins.

Press the buttons to write line 1 three ways, and watch line 3:

Playground: give Field a default
A name field and an age fieldPop out in the code editor (opens in a new tab)TypeScript
type Field<T> = { value: T; error: string | null }; const name: Field = { value: "Asha", error: null };//    ^?const age: Field<number> = { value: 30, error: null };console.log(name.value, age.value);
name's typeany
What happens
TypeScript1 error: line 3
When it runsprints Asha 30
Try it yourself

T has no default, so every use of Field must fill it in. Line 3 writes plain Field, and TypeScript says Generic type 'Field' requires 1 type argument(s).

TypeScript checks the code for real. Each button changes only line 1. Every version prints Asha 30 when it runs, because the types are gone by then. Reset goes back to No default.

With No default, plain Field is an error, as before. Default string fixes it. Default number shows that the default is a type, not a guess: plain Field now means Field<number>, and the text "Asha" doesn't fit. Line 5 never changes, because it writes its own type argument.

A type can have several type parameters, separated by commas, and only some of them need defaults:

Two type parameters, one with a defaultPop out in the code editor (opens in a new tab)TypeScript
type Field<Value, Problem = string> = { value: Value; error: Problem | null }; const email: Field<string> = { value: "asha@", error: "Add the part after @" };const password: Field<string, string[]> = { value: "abc", error: ["Too short", "Add a number"] };type PasswordField = Field<string, string[]>;//   ^?console.log(email.error, password.error?.length);

Line 1 names two type parameters: Value for the value and Problem for the error, which is text unless you say otherwise. Line 3 fills in only Value, so the email's error is text. Line 4 fills in both: a password field whose error is a list of problems. Line 5 names that type, and the query on line 6 shows it with error: string[] | null. Line 7 prints Add the part after @ 2: the email's message and the number of password problems. The ?. reads length only when error isn't null.

Type parameters with defaults go last. Otherwise, a single type argument like Field<string> would be unclear:

A default before a required type parameter (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type Field<Problem = string, Value> = { value: Value; error: Problem | null };

TypeScript says Required type parameters may not follow optional type parameters. A required type parameter has no default, so it must be filled in; an optional one has a default. Here Value is required and comes after Problem, which is optional. Swap them, as in the example before, and the error is gone.

Generic functions can have defaults too. A default is used when TypeScript has nothing to fill in T from:

A generic function with a defaultPop out in the code editor (opens in a new tab)TypeScript
function emptyList<T = string>(): T[] {  return [];} const tags = emptyList();//    ^?const prices = emptyList<number>();//    ^?console.log(tags, prices);

Line 1 says T = string. Line 5 calls emptyList() with no arguments, so nothing tells TypeScript what T is, and line 6 shows const tags: string[]. Without the default it would be unknown[], as in the Generic functions lesson. Line 7 writes <number>, and line 8 shows const prices: number[]. Line 9 prints [] [].

Generics you already use: Array, Map, Promise

You have been using generic types since the Arrays and tuples lesson. TypeScript's own types for arrays, Maps, Sets, and promises are generic, written the same way as yours:

Array, Map, and Set with type argumentsPop out in the code editor (opens in a new tab)TypeScript
const tags: Array<string> = ["new", "sale"];const quantities = new Map<string, number>();quantities.set("Tea", 2);const teaCount = quantities.get("Tea");//    ^?const sizes = new Set<string>(["S", "M"]);console.log(tags.length, teaCount, sizes.has("M"));

Line 1 writes Array<string>, the same type as string[]. Line 2 makes a Map, which stores values by key (the Map and Set lesson covers it). Its two type arguments say the keys are text and the values are numbers. Line 3 stores 2 for Tea. Line 4 reads it back, and the query on line 5 shows number | undefined: get gives back undefined for a key that isn't there. Line 6 makes a Set of sizes. Line 7 prints 2 2 true.

The type arguments are checked like any other type:

Text stored in a Map of numbers (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
const quantities = new Map<string, number>();quantities.set("Tea", 2);quantities.set("Cake", "1");console.log(quantities.get("Cake"));

Line 3 stores the text "1". TypeScript says Argument of type 'string' is not assignable to parameter of type 'number': the Map's values must be numbers. Run it anyway and line 4 prints 1, but it's text, and adding it to other quantities would join them.

Promise<T> is the type of a promise: a value that arrives later. T is the type of what it will hold. An async function always gives back a promise (see async and await):

A promise of a UserPop out in the code editor (opens in a new tab)TypeScript
type User = { name: string; email: string }; async function loadUser(): Promise<User> {  return { name: "Asha", email: "asha@example.com" };} const pending = loadUser();//    ^?pending.then((user) => console.log(user.name));//            ^?

Lines 3 to 5 declare loadUser, an async function. It returns a user object, but its return type is Promise<User>, because an async function wraps what it returns in a promise. Line 7 calls it. The query on line 8 shows const pending: Promise<User>: not a user yet, but a promise of one. Line 9 uses then, which calls a function with what the promise holds when it's ready. The query on line 10 shows (parameter) user: User: TypeScript took User out of Promise<User>. It prints Asha.

Press the buttons to read the user three ways on line 8:

Playground: get the user out of the promise
A user loaded by an async functionPop out in the code editor (opens in a new tab)TypeScript
type User = { name: string; email: string }; async function loadUser(): Promise<User> {  return { name: "Asha", email: "asha@example.com" };} async function showName() {  console.log(loadUser().name);}showName();
Console outputundefined
What happens
TypeScript1 error: line 8
Try it yourself

loadUser() gives back a Promise<User>, not a User. A promise has no name, so TypeScript says Property 'name' does not exist on type 'Promise<User>'. Run it anyway and it prints undefined.

TypeScript checks the code for real, and the output is what the code prints. Each button changes only line 8. Reset goes back to Read it right away.

Read it right away treats the promise as if it were the user, and TypeScript stops it. await it and Use then both wait for the user. The type argument of Promise<User> is what lets TypeScript know that what comes out is a User.

Generic types can go inside each other. A function that loads products from the server gives back a promise of a reply of a list:

A promise of a reply of productsPop out in the code editor (opens in a new tab)TypeScript
type Product = { name: string; price: number };type ApiResponse<T> = { status: number; data: T }; async function loadProducts(): Promise<ApiResponse<Product[]>> {  return { status: 200, data: [{ name: "Tea", price: 3 }, { name: "Cake", price: 4 }] };} async function showCount() {  const reply = await loadProducts();  //    ^?  console.log(reply.data.length);}showCount();

Line 4 says loadProducts gives back Promise<ApiResponse<Product[]>>. Read it from the outside in: a promise, holding an ApiResponse, whose data is a Product[]. Line 9 uses await, which opens the promise, and the query on line 10 shows const reply: ApiResponse<Product[]>. Line 11 reads reply.data.length and prints 2. Line 13 calls the function.

Three crates, one inside another. The outer crate is labelled Promise, the middle one ApiResponse, and the inner one Product[], holding Tea and Cake. Beside them: 1. await opens the Promise. 2. .data opens the ApiResponse. 3. Product[] is the list, with 2 products.Promise<ApiResponse<Product[]>>TeaCakeProduct[]ApiResponsePromise1. awaitopens the Promise2. .dataopens the ApiResponse3. Product[]the list: 2 products
Promise<ApiResponse<Product[]>> is one generic type inside another. Read it from the outside in: line 9's await opens the Promise, .data on line 11 opens the ApiResponse, and you reach the list of 2 products.

The table below lists the built-in generic types you'll meet most. Read each row as: the type, what fills in its blanks, and an example.

Built-in generic types
The generic typeWhat fills in its type parametersExample
Array<T>T is the type of each item. T[] is a shorter way to write the same type.Array<string>, the same as string[].
Map<K, V>K is the type of the keys, and V is the type of the values.Map<string, number> for quantities by product name.
Set<T>T is the type of each value in the set.Set<string> for the sizes a shopper picked.
Promise<T>T is the type of the value the promise gives you when it's ready.Promise<User> from an async function that loads a user.

Loading states for any page

Every page that loads data shows one of three things: a loading message, an error, or the data. A product list and a profile page load different data, but the three states are the same. One generic type describes them all:

One LoadState type for products and a profilePop out in the code editor (opens in a new tab)TypeScript
type Product = { name: string; price: number };type User = { name: string; email: string }; type LoadState<T> =  | { status: "loading" }  | { status: "error"; message: string }  | { status: "done"; data: T }; function productsText(state: LoadState<Product[]>) {  if (state.status === "loading") return "Loading...";  if (state.status === "error") return "Sorry: " + state.message;  const list = state.data;  //    ^?  return list.length + " products";} function profileText(state: LoadState<User>) {  if (state.status !== "done") return "No profile yet";  return "Hi " + state.data.name;} console.log(productsText({ status: "done", data: [{ name: "Tea", price: 3 }] }));console.log(productsText({ status: "error", message: "Offline" }));console.log(profileText({ status: "done", data: { name: "Asha", email: "asha@example.com" } }));

Lines 4 to 7 declare LoadState<T>: a union of three object types, told apart by status (the Discriminated unions lesson covers this pattern). Only the "done" state has data, and its type is T.

  • Line 9 takes a LoadState<Product[]>. Lines 10 and 11 handle loading and errors. After them, only "done" is left, so line 12 may read state.data, and line 13 shows const list: Product[].
  • Line 17 takes a LoadState<User>. After the check on line 18, line 19 reads state.data.name, because data is a User here.
  • Lines 22 to 24 print 1 products, Sorry: Offline, and Hi Asha.

The same type also stops you from reading data before it has loaded:

Data read before it has loaded (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type User = { name: string; email: string }; type LoadState<T> =  | { status: "loading" }  | { status: "error"; message: string }  | { status: "done"; data: T }; function profileText(state: LoadState<User>) {  return "Hi " + state.data.name;} console.log(profileText({ status: "loading" }));

Line 9 reads state.data without checking status first. TypeScript says Property 'data' does not exist on type 'LoadState<User>': the loading and error states have no data. Line 12 passes a loading state, and when the code runs it stops with a TypeError.

Error or fine?
  • type Box<T> = { value: T }; const price: Box<number> = { value: 3 };
  • type Box<T> = { value: T }; const price: Box = { value: 3 };
  • type Box<T> = { value: T }; const price: Box<string> = { value: 3 };
  • type Box<T = string> = { value: T }; const label: Box = { value: "Tea" };
  • const counts = new Map<string, number>(); counts.set("Tea", "2");
  • const tags: Array<string> = ["new", "sale"];
  • type Maybe<T> = T | null; const coupon: Maybe<string> = null;
  • async function load(): Promise<number> { return 3; } load().toFixed(2);
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

A type argument tells TypeScript what you expect. It doesn't check data that arrives while the page runs:

A reply that doesn't match its type argumentPop out in the code editor (opens in a new tab)TypeScript
type ApiResponse<T> = { status: number; data: T }; const text = '{ "status": 200, "data": "7" }';const reply: ApiResponse<number> = JSON.parse(text);console.log(reply.data + 1);

Line 3 is a reply as the server sent it, with data as the text "7". Line 4 parses it with JSON.parse (see the JSON lesson) and says it's an ApiResponse<number>. JSON.parse can give back anything, so TypeScript trusts you, and there are no errors. Line 5 prints 71, because "7" + 1 joins text.

The table below shows when you write a type argument. Read each row as: this code, do you need one, and why.

When you write the type argument
When you writeDo you need a type argument?Why
A generic type in an annotation: const name: FieldYes, unless T has a default.TypeScript doesn't work out a type's argument from the value you store.
A generic type whose T has a default: Field with T = stringNo.The default fills in T, so Field means Field<string>.
A call to a generic function: first(names)Usually not.TypeScript fills in T from the arguments.
An empty Map: new Map()Yes: new Map<string, number>().With nothing to go on, TypeScript makes it Map<any, any>, and checking stops.
  • "TypeScript fills in a generic type's argument from the value, like it does for a function." No. In an annotation like const name: Field, you write the type argument, or the type parameter needs a default.
  • "ApiResponse<number> checks the server's reply." No. Types are removed before the code runs. The reply with the text "7" got through and printed 71.
  • "A default type parameter changes to match the value." No. T = string means plain Field is always Field<string>. For a number, write Field<number>.
  • "A Promise<User> is a user." No. It's a promise that will hold one. Use await or then to get the user out.
  • "The T in one generic type is the same T as in another." No. Each declaration has its own type parameters. T in Field<T> and T in Page<T> are unrelated, like two functions that both have a parameter called value.
  • "Generic interfaces and generic type aliases work differently." No. For object shapes, you declare and use them the same way. Only a type alias can be a union, like Maybe<T>.

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. Try a guess first, then use the hints.

Exercise 1 · Warm-upRead the type a field holds

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

Starter codePop out in the code editor (opens in a new tab)TypeScript
type Field<T> = { value: T; error: string | null }; const quantity: Field<number> = { value: 2, error: null };const count = quantity.value;

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

    Exercise 2 · Warm-upFind the field TypeScript marks

    Three form fields, one with a mistake. Which line will TypeScript mark?

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    type Field<T> = { value: T; error: string | null };
    
    const email: Field<string> = { value: "asha@example.com", error: null };
    const age: Field<number> = { value: "30", error: null };
    const agree: Field<boolean> = { value: true, error: null };

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

      Exercise 3 · PracticeTurn two page types into one

      ProductPage and SizePage are copies with a different item type. Replace them with one generic type. What do you write instead of SizePage on line 7?

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      type Product = { name: string; price: number }; type ProductPage = { items: Product[]; page: number };type SizePage = { items: string[]; page: number }; const products: ProductPage = { items: [{ name: "Tea", price: 3 }], page: 1 };const sizes: SizePage = { items: ["S", "M", "L"], page: 2 };console.log(products.items.length + sizes.items.length);

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

        Exercise 4 · PracticeFill in the reply's type argument

        TypeScript marks line 4. Fix it with a type argument. What goes in the angle brackets?

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        type User = { name: string; email: string };type ApiResponse<T> = { status: number; data: T }; const reply: ApiResponse = { status: 200, data: { name: "Asha", email: "asha@example.com" } };console.log(reply.data.name);

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

          Exercise 5 · ChallengeGive Field a default

          Lines 3 and 4 use plain Field, and TypeScript marks both. Change only line 1 so they work and line 5 still holds a number. What does the code print then?

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          type Field<T> = { value: T; error: string | null }; const firstName: Field = { value: "Asha", error: null };const lastName: Field = { value: "Rao", error: null };const age: Field<number> = { value: 30, error: null };console.log(firstName.value, lastName.value, age.value);

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

            Exercise 6 · ChallengeGive a Map its type arguments

            A cart counts quantities in a Map. TypeScript reports no errors. What does the starter print? Then give the Map type arguments, and fix what TypeScript finds.

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            const quantities = new Map();quantities.set("Tea", 2);quantities.set("Cake", "1"); let total = 0;for (const count of quantities.values()) {  total += count;}console.log(total);

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

              Check your understanding

              For each question, find each generic type's blanks and what fills them: a type argument you wrote, or a default. The quiz shows its code without the checker, so the error list doesn't give the answers away.

              The generic types quiz · 8 questionsScore: first tries count
              1. Question 1 of 8What is T in type ApiResponse<T> = { status: number; data: T }?

                Choose an answer to see the explanation.

              2. Question 2 of 8What type does people get from this reply?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type User = { name: string; email: string };
                type ApiResponse<T> = { status: number; data: T };
                
                const reply: ApiResponse<User[]> = { status: 200, data: [] };
                const people = reply.data;

                Choose an answer to see the explanation.

              3. Question 3 of 8Which line does TypeScript flag in these form fields?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type Field<T> = { value: T; error: string | null };
                
                const email: Field<string> = { value: "asha@example.com", error: null };
                const phone: Field = { value: "555-0100", error: null };

                Choose an answer to see the explanation.

              4. Question 4 of 8What does TypeScript say about line 3, where Field has a default?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type Field<T = string> = { value: T; error: string | null };
                
                const age: Field = { value: 30, error: null };

                Choose an answer to see the explanation.

              5. Question 5 of 8What type does result have when you call loadUser() without await?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type User = { name: string; email: string };
                
                async function loadUser(): Promise<User> {
                  return { name: "Asha", email: "asha@example.com" };
                }
                
                const result = loadUser();

                Choose an answer to see the explanation.

              6. Question 6 of 8What type does cakes get from this Map?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                const stock = new Map<string, number>([["Tea", 5]]);
                const cakes = stock.get("Cake");

                Choose an answer to see the explanation.

              7. Question 7 of 8What does this reply print, and does TypeScript complain?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type ApiResponse<T> = { status: number; data: T };
                
                const reply: ApiResponse<number> = JSON.parse('{ "status": 200, "data": "2" }');
                console.log(reply.data + 1);

                Choose an answer to see the explanation.

              8. Question 8 of 8When can you use a generic type without writing a type argument?

                Choose an answer to see the explanation.

              Key takeaways

              • A generic type is a type alias or an interface with a type parameter, like ApiResponse<T>. Write it once, instead of one copy per kind of data.
              • Each use fills in T with a type argument, like ApiResponse<User>, and TypeScript reads the type with T replaced.
              • In an annotation, you write the type argument. TypeScript doesn't work it out from the value.
              • A default, like <T = string>, is used when you leave the type argument out. A type argument you write wins. Type parameters with defaults go last.
              • Array<T>, Map<K, V>, Set<T>, and Promise<T> are generic types too. Read nested ones, like Promise<ApiResponse<Product[]>>, from the outside in.
              • A type argument is something you tell TypeScript. Data that arrives while the page runs isn't checked against it.

              Remember the one-liner.
              A generic type is a type with a blank: write it once, and fill in the blank each time you use it.

              Coming next: Generic classes. It puts type parameters on classes, for a typed stack or cache that keeps its item type.

              CompleteFrontend Clear concepts. Working examples.