Generic functions
Write one TypeScript function that works for names, prices, or products and keeps their types, with type parameters, inference, and type arguments.
- 01Write a generic functionAdd a type parameter like
<T>and use it to connect what goes into a function with what comes out. - 02Read and write type argumentsPredict the type TypeScript fills in from the arguments, and write
<string[]>yourself when the arguments don't say enough. - 03Choose generics over anyExplain why a type parameter keeps checking on while
anyturns it off, and when a function doesn't need to be generic.
One function for every kind of list
A shop page has many lists: product names, prices, orders. Often you need the same small job on each of them, like getting the first item. Here is a first function typed for a list of text:
function first(list: string[]): string | undefined { return list[0];} const names = ["Tea", "Cake"];console.log(first(names));const prices = [3, 4];console.log(first(prices));Line 1 says first takes a list that is a string[], which means an array of strings. It gives back string | undefined: the first item, or undefined when the list is empty. Line 2 reads the item at position 0 (the Array basics lesson covers arrays). Line 6 prints Tea. Line 8 passes prices, an array of numbers, and TypeScript marks it, on purpose:
Argument of type 'number[]' is not assignable to parameter of type 'string[]'means "you passed a list of numbers where a list of text has to go". An argument is the value you pass in a call.
Run it anyway and line 8 prints 3. The function's code works for any list; only its types say text. You could copy it as firstPrice(list: number[]), and again for every other kind of list. A generic function writes it once:
function first<T>(list: T[]): T | undefined { return list[0];} const names = ["Tea", "Cake"];const firstName = first(names);// ^?const prices = [3, 4];const firstPrice = first(prices);// ^?console.log(firstName, firstPrice);Line 1 has one new part: <T> after the name. T is a type parameter: a name that stands for a type, the way list stands for a value. list: T[] says the list holds items of type T, and T | undefined says first gives back one of those items, or undefined.
Line 6 calls first(names). TypeScript sees an array of strings, so for this call T is string. The query on line 7 shows const firstName: string | undefined. Line 9 calls it with the prices, so T is number, and line 10 shows const firstPrice: number | undefined. There are no errors, and line 11 prints Tea 3.
first, called on lines 6 and 9. Each call fills in T from its list: string for the names, number for the prices. What comes out has the type that went in.At a theatre's coat check, one desk takes coats, umbrellas, and bags. When you hand something in, the attendant writes what it is on your ticket. At the end of the night, the ticket says "coat", so you get a coat back, not just "some item".
- In real life: You hand in a coat
- In TypeScript: You pass an argument, like
prices - In real life: The attendant writes "coat" on your ticket
- In TypeScript: TypeScript fills in
Tfrom the argument:number - In real life: You get your coat back, not "some item"
- In TypeScript:
first(prices)gives backnumber | undefined - In real life: The same desk takes umbrellas and bags too
- In TypeScript: The same
firstworks for a list of anything
A generic function has a type parameter, a name in angle brackets like <T> that stands for a type. Each call fills it in with a real type, usually worked out from the arguments. Because T appears in both the parameter and the return type, what comes out has the same type as what went in.
Type parameters: a name for a type
A generic function has two kinds of parameters:
- Type parameters, in angle brackets before the round brackets:
<T>. They stand for types, and they're removed before the code runs. - Value parameters, in the round brackets as usual:
(list: T[]). They hold the values you pass.
Once declared, T can go anywhere a type goes in that function: on a parameter, in the return type, or inside the body. In first, it appears twice, in list: T[] and in T | undefined. That is what connects them: whatever type the list's items have, the result has the same type.
T is only a name, short for "type". Most code uses it for a function's one type parameter, but <Item> with list: Item[] works the same way. Inside the function, TypeScript doesn't know which type T will be, so it only allows what works for every type:
function shout<T>(value: T) { return value.toUpperCase();} console.log(shout("sale"));Line 1 declares T, and line 2 calls value.toUpperCase(). TypeScript marks it: Property 'toUpperCase' does not exist on type 'T'. In plain words: T could be a number, and numbers have no toUpperCase. Line 5 passes text, so the code prints SALE when it runs, but TypeScript checks the function for every T, not just for this call. The next lesson, Generic constraints, shows how to say "T must be text" or "T must have a length".
Arrow functions take type parameters too, written just before the round brackets:
const last = <T>(list: T[]) => list.at(-1);// ^? const sizes = ["S", "M", "L"];const lastSize = last(sizes);// ^?console.log(lastSize);Line 1 makes last, an arrow function with a type parameter T. list.at(-1) reads the last item: at counts from the end when you give it a negative number, and gives back undefined for an empty list. The query on line 2 shows the whole function's type: const last: <T>(list: T[]) => T | undefined. You didn't write a return type; TypeScript worked out T | undefined from at. Line 5 calls it with sizes, line 6 shows const lastSize: string | undefined, and line 7 prints L.
In a React .tsx file, <T> before an arrow function looks like the start of a JSX tag. Write <T,> there, with a comma, and it reads as a type parameter.
TypeScript fills in T from the arguments
You never wrote string or number in the overview's calls. TypeScript worked out T from the arguments. The type that fills in a type parameter is called a type argument, and TypeScript working it out for you is called inference. Try it: type any list of values inside the brackets on line 5, and watch the answer after ^? on line 6.
function first<T>(list: T[]): T | undefined { return list[0];} const item = first(["Tea", "Cake"]);// ^?console.log(item);["Tea", "Cake"]string | undefinedprints TeaThe list holds text, so TypeScript fills in T as string, and item is string | undefined. The code prints Tea.
With 3, 4, item is number | undefined. With "Tea", 3, it's string | number | undefined: when the items differ, T becomes a union of their types. An empty box gives undefined: with no items, T is never, a type with no values at all, and never | undefined is just undefined. Tea without quotes gets Cannot find name 'Tea', because it's a name, not text.
When a type parameter appears in two parameters, TypeScript fills it in from both arguments, and they must agree:
function pair<T>(a: T, b: T): T[] { return [a, b];} const sizes = pair("S", "M");// ^?const mixed = pair(3, "Tea");Line 1 says a and b are both T, and pair gives back a T[] holding both. Line 5 passes two strings, so T is string, and line 6 shows const sizes: string[]. Line 7 passes 3 and "Tea". TypeScript fills in T as number from the first argument, then marks the second: Argument of type 'string' is not assignable to parameter of type 'number'. In plain words: T is already number for this call, and text doesn't fit. The next section shows how to allow both.
A function can have more than one type parameter, separated by commas. TypeScript fills in each one, even from a callback, a function you pass in for another function to call:
function mapList<Input, Output>(list: Input[], change: (item: Input) => Output): Output[] { return list.map(change);} const labels = mapList([3, 4], (price) => "$" + price);// ^?// ^?console.log(labels);Line 1 declares two type parameters, Input and Output. list: Input[] is the list. change: (item: Input) => Output is a function type: "a function that takes an Input and gives back an Output". The result is Output[]. Line 2 uses the array's own map (see map, filter, and reduce).
Line 5 passes [3, 4], so Input is number. The query on line 6 points at the callback's parameter: (parameter) price: number. You didn't write a type for price; TypeScript passed Input down to it. The callback gives back "$" + price, which is text, so Output is string, and line 7 shows const labels: string[]. Line 8 prints [ '$3', '$4' ].
Writing the type argument yourself
You can also write the type argument yourself, in angle brackets after the function's name, just before the round brackets:
function first<T>(list: T[]): T | undefined { return list[0];} const firstName = first<string>(["Tea", "Cake"]);// ^?const firstPrice = first<number>(["Tea"]);Line 5 writes first<string>, so T is string for this call, the same type TypeScript would fill in. Line 6 shows const firstName: string | undefined. Line 7 writes first<number> and passes a list of text. Now the type you wrote wins, and TypeScript checks the list against it: Type 'string' is not assignable to type 'number' means "this item is text, and you said the list holds numbers".
A written type argument helps when the type TypeScript would fill in isn't the one you want. The pair from the last section can hold a number and text if you say so:
function pair<T>(a: T, b: T): T[] { return [a, b];} const mixed = pair<number | string>(3, "Tea");// ^?console.log(mixed);Line 5 says T is number | string, either one. Both arguments fit, and line 6 shows const mixed: (string | number)[]: an array whose items are each a string or a number. Line 7 prints [ 3, 'Tea' ].
The most common case is an empty list. A page reads a saved cart: localStorage.getItem("cart") gives back the saved text, or null when nothing was saved (the Web storage lesson covers it). parseOr turns the text into a value with JSON.parse (see the JSON lesson), or uses a fallback when there's nothing. Here nothing was saved yet, so the fallback, an empty list, becomes the cart. Press the buttons to write line 6 four ways:
function parseOr<T>(text: string | null, fallback: T): T { return text === null ? fallback : JSON.parse(text);} const savedCart = null; // nothing saved yetconst cart = parseOr(savedCart, []);// ^?cart.push("Tea");console.log(cart);never[]1 error: line 8prints [ 'Tea' ]Nothing says what the empty list [] will hold, so TypeScript fills in T as never[]: a list that can hold nothing. Line 8 gets Argument of type '"Tea"' is not assignable to parameter of type 'never'.
With No type argument, the fallback [] is TypeScript's only clue, and an empty list says nothing about its items. TypeScript fills in never[], so line 8 can't push anything. <string[]> fixes it. <number[]> shows that a type you write is checked too. <string> is a common slip: T stands for the whole fallback, the list, not one item in it.
never[]. Writing <string[]> tells it, and line 8 may push "Tea".If no argument mentions T at all, TypeScript has nothing to go on and fills in unknown, a type that means "this could be anything, so check it before you use it":
function emptyList<T>(): T[] { return [];} const tags = emptyList();// ^?const sizes = emptyList<string>();// ^?Line 1 declares T, but no parameter uses it. Line 5 calls emptyList(), and line 6 shows const tags: unknown[]. Line 7 writes <string>, and line 8 shows const sizes: string[].
The table below shows how T gets its type. Read each row as: this call, what T becomes, and why.
| When you write | What T becomes | Why |
|---|---|---|
first(["Tea", "Cake"]) | string | The list holds text, so TypeScript fills in string. |
first(["Tea", 3]) | string | number | The list holds text and a number, so T covers both. |
parseOr(savedCart, []) | never[] | An empty list says nothing about its items. never[] is a list that can hold nothing. |
emptyList() | unknown | No argument mentions T, so TypeScript uses unknown: it could be anything. |
first<number>(["Tea"]) | number | You wrote number, so the list must hold numbers, and "Tea" gets an error. |
Generics or any?
Before generics, the usual way to write one function for every list was any. any is a type that turns checking off: every value fits it, and anything you do with it is allowed. That sounds handy, but TypeScript stops helping. Press the buttons to write line 1 three ways:
function first(list: any[]): any { return list[0];} const prices = [3, 4];const price = first(prices);// ^?console.log(price?.toUpperCase());anyNo errorsstops with a TypeErrorany turns checking off. price is any, so TypeScript allows price?.toUpperCase() with no error. When the code runs, price is the number 3, and it stops with a TypeError.
Line 8 has the same mistake every time: it calls toUpperCase, a method for text, on a price. The ?. calls it only when price isn't undefined (see Nullish coalescing and optional chaining).
- With any, there are no errors, and the shopper's page throws a
TypeError. - With Only text, TypeScript marks line 6, because this
firstdoesn't take numbers at all. - With Type parameter T, TypeScript marks the real mistake on line 8.
priceisnumber | undefined, so afterprice?.your editor suggests number methods liketoFixed.
first(prices), on line 6. With any, TypeScript forgets that the prices are numbers. With T, it keeps number, so line 8's toUpperCase gets an error.The table below compares the two ways to write one function for every list. Read each row as a question, then each answer.
| Question | With any | With a type parameter T |
|---|---|---|
What does first(prices) give back? | any. TypeScript forgets that the prices are numbers. | number | undefined. The same type that went in. |
Does price?.toUpperCase() get an error? | No. any turns checking off, so the mistake reaches the shopper as a TypeError. | Yes. Numbers have no toUpperCase, so TypeScript marks the line. |
Does autocomplete help after price.? | No. The editor can't know what an any value has. | Yes. It lists what numbers have, like toFixed. |
| Can it take a list of anything? | Yes. | Yes. |
Use a type parameter when it connects two places: a parameter and the return type, like first, or two parameters, like pair. If T appears only once, it connects nothing, and a plain type is clearer: write function logTotal(total: number) rather than function logTotal<T>(total: T).
A search box that filters any list
A search box on a shop page filters products as the shopper types. The admin page filters users with the same kind of box. One generic search does both, and keeps each list's type:
type Product = { name: string; price: number };type User = { name: string; email: string }; function search<T>(items: T[], matches: (item: T) => boolean): T[] { return items.filter(matches);} const products: Product[] = [ { name: "Tea", price: 3 }, { name: "Cake", price: 4 },];const users: User[] = [ { name: "Asha", email: "asha@example.com" }, { name: "Carla", email: "carla@example.com" },]; const typed = "ca";const shownProducts = search(products, (product) => product.name.toLowerCase().includes(typed));// ^?const shownUsers = search(users, (user) => user.email.includes(typed));// ^?console.log(shownProducts.map((product) => product.price));console.log(shownUsers.map((user) => user.name));Lines 1 and 2 use type to give a name to the shape of a product and of a user. Line 4 declares search with one type parameter, T:
items: T[]is the list.matches: (item: T) => booleanis a function that takes one item and saystrue(keep it) orfalse.T[]after the brackets sayssearchgives back a list of the same items.
Line 5 keeps the items matches says yes to, with filter. Line 17 is the text typed in the box, "ca". Line 18 searches the products, so T is Product and TypeScript knows product.name is text. Line 19 shows const shownProducts: Product[]. Line 20 searches the users by email, and line 21 shows const shownUsers: User[]. Line 22 prints [ 4 ], the price of Cake, and line 23 prints [ 'Carla' ].
Because T is filled in for each call, a mistake inside the callback is caught too:
type User = { name: string; email: string }; function search<T>(items: T[], matches: (item: T) => boolean): T[] { return items.filter(matches);} const users: User[] = [ { name: "Asha", email: "asha@example.com" }, { name: "Carla", email: "carla@example.com" },];const cheap = search(users, (user) => user.price < 4);console.log(cheap);Line 11 searches the users but reads user.price, a slip copied from a product filter. T is User, and users have no price: Property 'price' does not exist on type 'User'. Run it anyway and it prints [], because undefined < 4 is false for every user. With any, nothing would point at line 11.
function first<T>(list: T[]) { return list[0]; } first<number>(["Tea"]);function first<T>(list: T[]) { return list[0]; } first(["Tea", 3]);function pair<T>(a: T, b: T) { return [a, b]; } pair(3, "Tea");function pair<T>(a: T, b: T) { return [a, b]; } pair<number | string>(3, "Tea");function shout<T>(value: T) { return value.toUpperCase(); }function wrap<T>(value: T): T[] { return [value]; } wrap("Tea").push("Cake");function wrap<T>(value: T): T[] { return [value]; } wrap(3).push("Cake");function firstAny(list: any[]): any { return list[0]; } firstAny([3]).toUpperCase();
Sort each line by what TypeScript says about it. Each line is checked on its own.
Common mix-ups
Type parameters are types, and types are removed before the code runs. So you can't ask for T while the code runs:
function describe<T>(value: T) { console.log(typeof value); console.log(T);} describe(3);Line 2 is fine: typeof value asks the value itself, and prints number. Line 3 uses T as if it were a value. TypeScript says 'T' only refers to a type, but is being used as a value here. When the code runs, there's no T left, and it stops with a ReferenceError.
- "A type parameter is a value I can check while the code runs." No.
Tis removed with the other types. Check the value instead, withtypeof value. - "any does the same job as a type parameter." No. Both accept a list of anything, but
anyforgets the type and turns checking off.Tkeeps the type, so the result is checked. - "I have to write the type argument on every call." No. TypeScript fills it in from the arguments. Write it when the arguments don't say enough, like an empty list.
- "The type argument is the type of one item." No. It's whatever
Tstands for. InparseOr,Tis the whole fallback, so a list of text is<string[]>, not<string>. - "Inside the function, I can use any method on a T value." No.
Tcould be any type, so only what every type has is allowed. The next lesson shows how to ask for more. - "Every function should be generic, just in case." No. A type parameter is useful when it connects two places. If it appears once, a plain type is clearer.
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.
No type argument is written here. What type does TypeScript give size? Guess first. Then point at size, or press Edit & run and add a // ^? line under it.
function first<T>(list: T[]): T | undefined { return list[0];} const sizes = ["S", "M", "L"];const size = first(sizes);function first<T>(list: T[]): T | undefined { return list[0];} const sizes = ["S", "M", "L"];const size = first(sizes);// ^?sizes is a string[], so for this call T is string, and first gives back string | undefined. The query on line 7 shows const size: string | undefined.
Three calls to pair. Which line will TypeScript mark?
function pair<T>(a: T, b: T): T[] {
return [a, b];
}
const sizes = pair("S", "M");
const prices = pair(3, 4);
const order = pair("Tea", 2);function pair<T>(a: T, b: T): T[] { return [a, b];} const sizes = pair("S", "M");const prices = pair(3, 4);const order = pair("Tea", 2); // line 7: T is already stringOn line 7, "Tea" makes T a string, so the number 2 doesn't fit: Argument of type 'number' is not assignable to parameter of type 'string'. Lines 5 and 6 pass two values of the same type.
last only takes a list of text, so TypeScript marks line 8. Make it generic so it takes any list. What does line 8 print then?
function last(list: string[]): string | undefined { return list[list.length - 1];} const sizes = ["S", "M", "L"];const prices = [3, 4];console.log(last(sizes));console.log(last(prices));function last<T>(list: T[]): T | undefined { return list[list.length - 1];} const sizes = ["S", "M", "L"];const prices = [3, 4];console.log(last(sizes));console.log(last(prices));With <T>, line 7 fills in T as string and line 8 fills it in as number, so both calls are allowed. Line 8 prints the last price, 4.
The saved tags start as an empty list, and TypeScript marks line 7. Fix line 6 with a type argument. What goes in the angle brackets?
function parseOr<T>(text: string | null, fallback: T): T { return text === null ? fallback : JSON.parse(text);} const savedTags = null; // nothing saved yetconst tags = parseOr(savedTags, []);tags.push("sale");console.log(tags.length);function parseOr<T>(text: string | null, fallback: T): T { return text === null ? fallback : JSON.parse(text);} const savedTags = null; // nothing saved yetconst tags = parseOr<string[]>(savedTags, []);tags.push("sale");console.log(tags.length);parseOr<string[]> says the fallback is a list of text, so tags is string[] and line 7 may push "sale". The code prints 1.
The last product a shopper viewed. TypeScript reports no errors. What does the starter print? Then replace any with a type parameter and fix what TypeScript finds.
function lastViewed(items: any[]): any { return items[items.length - 1];} type Product = { name: string; price: number };const viewed: Product[] = [ { name: "Tea", price: 3 }, { name: "Cake", price: 4 },];const product = lastViewed(viewed);console.log(product.prise);function lastViewed<T>(items: T[]): T | undefined { return items[items.length - 1];} type Product = { name: string; price: number };const viewed: Product[] = [ { name: "Tea", price: 3 }, { name: "Cake", price: 4 },];const product = lastViewed(viewed);console.log(product?.price);The starter prints undefined: a product has no prise, and any hid the typo. With T, product is Product | undefined, so TypeScript says 'product' is possibly 'undefined' and, for the typo, Did you mean 'price'?. product?.price fixes both and prints 4.
A heart button on a product adds it to your favorites, or takes it out if it's already there. includes checks whether the list has the item, and [...list, item] makes a new list with it added (see Spread and rest). TypeScript marks the parameters, because they have no types. Add a type parameter so there are no errors. What does the code print?
function toggle(list, item) { return list.includes(item) ? list.filter((saved) => saved !== item) : [...list, item];} const favorites = toggle(["Tea", "Cake"], "Tea");console.log(favorites);function toggle<T>(list: T[], item: T): T[] { return list.includes(item) ? list.filter((saved) => saved !== item) : [...list, item];} const favorites = toggle(["Tea", "Cake"], "Tea");// ^?console.log(favorites);T connects the list, the item, and the result. Line 5 fills in T as string, so favorites is string[]. "Tea" is already saved, so filter takes it out, and the code prints [ 'Cake' ].
Check your understanding
For each question, ask what T is for that call: filled in from the arguments, or written in angle brackets. The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 7What does
<T>declare infunction first<T>(list: T[])?Choose an answer to see the explanation.
Question 2 of 7What type does TypeScript give
pricehere?Read the code, then predictfunction first<T>(list: T[]): T | undefined { return list[0]; } const price = first([3, 4]);Choose an answer to see the explanation.
Question 3 of 7Which line does TypeScript flag in these two pairs?
Read the code, then predictfunction pair<T>(a: T, b: T): T[] { return [a, b]; } const sizes = pair("S", "M"); const order = pair("Tea", 2);Choose an answer to see the explanation.
Question 4 of 7What type does
cartget when the fallback is an empty list?Read the code, then predictfunction parseOr<T>(text: string | null, fallback: T): T { return text === null ? fallback : JSON.parse(text); } const cart = parseOr(null, []);Choose an answer to see the explanation.
Question 5 of 7What happens with this version that uses any?
Read the code, then predictfunction first(list: any[]): any { return list[0]; } const price = first([3, 4]); console.log(price.toUpperCase());Choose an answer to see the explanation.
Question 6 of 7What does TypeScript say about the type argument on line 5?
Read the code, then predictfunction first<T>(list: T[]): T | undefined { return list[0]; } const price = first<number>(["Tea"]);Choose an answer to see the explanation.
Question 7 of 7When should you write a type argument yourself?
Choose an answer to see the explanation.
Key takeaways
- A type parameter, like
<T>, is a name for a type. Each call fills it in, so one function works for every kind of list. - Using
Tin a parameter and in the return type connects them:first(prices)gives backnumber | undefined. - TypeScript usually fills in
Tfrom the arguments. When items differ,Tbecomes a union. - Write the type argument yourself, like
<string[]>, when the arguments don't say enough, such as an empty list. anyturns checking off and forgets the type.Tkeeps the type, so mistakes are still caught.- Inside the function, only what works for every type is allowed, and
Tis gone when the code runs.
Remember the one-liner.
A type parameter is a blank for a type: each call fills it in, so what comes out is as precise as what went in.
Coming next: Generic constraints. It shows how to say what T must have, like a length, so you can use it inside the function.