Function types
Describe a function as a type, so callbacks, event handlers, and props get checked, and see how TypeScript types a callback from where you pass it.
- 01Write a function's typeDescribe a function as a value with
(price: number) => string, and give it a name with a type alias. - 02Type callbacks safelyType the functions you pass to other functions, and read the errors when a callback doesn't fit.
- 03Use call signatures and contextDescribe a function that also has properties, and know when TypeScript works out a callback's parameter types for you.
A function that takes another function
In JavaScript you pass functions to other functions all the time: a click handler to addEventListener, a rule to map, a "tell me when you're done" function to a save. A function passed like this is called a callback (the Callbacks lesson covers them). Here a cart's save function takes one:
function saveCart(onSaved) {
onSaved(3);
}
saveCart((count) => console.log("Saved " + count + " items"));
saveCart("done");Line 1 makes saveCart, which takes a function called onSaved. Line 2 calls it with 3, the number of items saved. Line 5 passes an arrow function, and it prints Saved 3 items. Line 6 passes the text "done" instead. When line 2 tries to call it, JavaScript throws TypeError: onSaved is not a function, and the code stops. Nothing warned you before it ran.
In the Typing functions lesson, you wrote types like price: number on parameters. A parameter that holds a function needs a type too: one that says how the function will be called. That is a function type:
function saveCart(onSaved: (count: number) => void) { onSaved(3);} saveCart((count) => console.log("Saved " + count + " items"));saveCart("done");Line 1 now says onSaved: (count: number) => void. Read it as "a function that takes a number called count and gives back nothing to use". Line 5 passes such a function, so it's fine. Line 6 gets an error, on purpose here, because it's the bug:
Argument of type 'string' is not assignable to parameter of type '(count: number) => void'means "you passed text where a function has to go". An argument is the value you pass in a call.
count. After the => comes what it gives back. Here it's void: nothing the caller uses.You order cakes and leave your phone number at the counter. The bakery says how the call will go: "We'll ring and tell you how many cakes are ready." Any phone that can take that call will do. If you leave a shopping list on the counter instead, nobody can ring it.
- In real life: You leave a phone number at the bakery
- In TypeScript: You pass a callback to
saveCart - In real life: The bakery says: we'll call and tell you how many cakes are ready
- In TypeScript: The type says:
(count: number) => void - In real life: They tell you a number, and don't wait for you to say anything back
- In TypeScript:
countis a number, andvoidmeans the result is ignored - In real life: A shopping list left instead of a phone number can't be called
- In TypeScript:
"done"isn't a function, so it can't be called
A function type describes a function as a value: the parameters it takes and the type it gives back, like (count: number) => void. Give a parameter, a variable, or a property a function type, and TypeScript checks every function you put there.
Function type expressions
The type (count: number) => void is called a function type expression. It looks like an arrow function, but it's a type, so it never runs. It has three parts:
- The parameters in brackets, each with a name and a type:
(price: number). - An arrow,
=>. - The type the function gives back:
string, orvoidfor nothing to use.
Writing a long function type over and over is tiring, so you can give it a name. type PriceFormatter = … is a type alias: a name for a type, which you can use anywhere a type goes. You met them in Type aliases & interfaces, and a function type works the same way.
type PriceFormatter = (price: number) => string; const format: PriceFormatter = (price) => "$" + price.toFixed(2);// ^?console.log(format(4));Line 1 names the type PriceFormatter: a function that takes a number and returns a string. Line 3 stores an arrow function in format and says it's a PriceFormatter. Notice that (price) has no type written. The query on line 4 shows TypeScript still knows it: (parameter) price: number. It took the type from PriceFormatter. Line 5 prints $4.00.
Which functions can go on line 3? Try five, and watch the list under the code:
type PriceFormatter = (price: number) => string; const format: PriceFormatter = (price) => "$" + price.toFixed(2);console.log(format(4));$4.00(price) => "$" + price.toFixed(2)No errorsThe function takes a number and returns text, exactly what PriceFormatter describes. TypeScript has nothing to report, and line 4 prints $4.00.
Matches and Takes nothing have no errors. A function may take fewer parameters than the type lists, because JavaScript ignores extra arguments. Returns a number fails because a PriceFormatter must give back a string. Takes text fails because callers will pass a number. Needs two fails because callers pass only one argument, so currency would always be undefined. In each error, the first line names the two types, and the indented line under it gives the reason.
One thing catches almost everyone. In a function type, every parameter needs a name, and the name comes first:
type PriceFormatter = (number) => string;You might read (number) => string as "takes a number". TypeScript reads it as a parameter named number, with no type. With strict checking on, as in this lesson, it says Parameter has a name but no type. Did you mean 'arg0: number'? Write a name and a type: (price: number) => string.
Typing callbacks
When you write a function that takes a callback, the callback's type is a promise about how you'll call it. Here is a small function that calls a callback once for each price, the way forEach does:
type PriceCallback = (price: number, index: number) => void; function forEachPrice(prices: number[], callback: PriceCallback) { for (let i = 0; i < prices.length; i++) callback(prices[i], i);} forEachPrice([3, 4], (price, index) => console.log(index + ": " + price));// ^?Line 1 names the callback's type: it gets a price and an index, both numbers, and gives back nothing to use. Line 3 says forEachPrice takes a list of numbers and a PriceCallback. Line 4 loops over the prices and calls the callback with each price and its position. Line 7 passes a callback, and the query on line 8 shows price is a number. It prints 0: 3, then 1: 4.
Now pick other callbacks for line 7:
type PriceCallback = (price: number, index: number) => void; function forEachPrice(prices: number[], callback: PriceCallback) { for (let i = 0; i < prices.length; i++) callback(prices[i], i);} forEachPrice([3, 4], (price, index) => console.log(index + ": " + price));0: 3, then 1: 4(price, index) => console.log(index + ": " + price)No errorsThe callback takes both parameters. price is a number and index is a number, both worked out from PriceCallback.
Uses the price takes one parameter, and that's fine. Returns a value is fine too: more on that below. Expects text gets an error, and running it anyway stops with a TypeError. Wants three gets an error because forEachPrice never passes a third value.
3 and 0. The Uses the price callback takes only price, so the index is passed and ignored. Taking fewer parameters is always allowed.The table below sums up what a callback may do. Read each row as: the callback does this, then what TypeScript says, and why.
| When the callback… | What TypeScript says | Why |
|---|---|---|
Takes fewer parameters than the type lists, like (price) => … | Fine. | JavaScript ignores extra arguments, so a callback can skip the ones it doesn't need. |
| Takes more parameters than the type lists | An error: Target signature provides too few arguments. | The caller never passes them, so they would be undefined. |
Expects a different type, like price: string | An error: Types of parameters 'price' and 'price' are incompatible. | The caller passes a number, and the callback would treat it as text. |
Returns a value when the type says void | Fine. | void in a function type means the caller ignores whatever comes back. |
Returns a number when the type says string | An error: Type 'number' is not assignable to type 'string'. | The caller will use the result as text. |
Don't make callback parameters optional
Since a callback may skip the index, you might think the type should mark it optional with ?. That says something else: "I might call you without an index". TypeScript then makes every callback deal with a missing one:
type PriceCallback = (price: number, index?: number) => void; function forEachPrice(prices: number[], callback: PriceCallback) { for (let i = 0; i < prices.length; i++) callback(prices[i], i);} forEachPrice([3, 4], (price, index) => console.log(index.toFixed()));Line 1 writes index?: number. Line 4 always passes the index, but TypeScript only reads the type. So on line 7 it says 'index' is possibly 'undefined', an error for something that can't happen. When it runs, it prints 0 and 1. Write callback parameters without ?, unless you really call the callback without them.
A void callback may return a value
In the playground, Returns a value had no error, even though PriceCallback returns void. Compare it with a function you write with : void yourself:
const saved: number[] = [];[3, 4].forEach((price) => saved.push(price));console.log(saved.join(", ")); const logPrice = function (price: number): void { return price;};On line 2, forEach's callback type returns void, and saved.push(price) returns a number (the new length). That's allowed: void in a function type means "the caller ignores what comes back". Line 3 prints 3, 4. Lines 5 to 7 are different. There, : void is written on the function itself, so it must not return a value, and line 6 gets Type 'number' is not assignable to type 'void', as in the last lesson.
Call signatures: functions with properties
A JavaScript function is also an object, so it can carry properties (the Functions are objects lesson shows how). A function type expression can't list properties. For a function that has some, write a call signature inside an object type:
type PriceRule = { label: string; (total: number): number;}; function tenOff(total: number) { return total - 10;}tenOff.label = "10 off"; function applyRule(rule: PriceRule, total: number) { console.log(rule.label + ": " + rule(total));} applyRule(tenOff, 50);applyRule((total: number) => total / 2, 50);Lines 1 to 4 describe a PriceRule. Line 2 is an ordinary property: a label that is text. Line 3 is the call signature: it says a PriceRule can be called with a number and gives back a number. Notice it uses a colon before the return type, not an arrow.
Lines 6 to 8 declare tenOff, and line 9 gives it a label. TypeScript notices a property you add to a declared function like this, so tenOff now has both parts. Line 12 reads the label and calls the rule. Line 15 passes tenOff and prints 10 off: 40.
Line 16 passes an arrow function. It can be called the right way, but it has no label, so TypeScript reports it, on purpose here. The first line of the error says the arrow function isn't a PriceRule, and the line under it gives the reason: Property 'label' is missing in type '(total: number) => number' but required in type 'PriceRule'. Run it anyway and line 12 prints undefined: 25.
tenOff is one function you can call and read a property from. PriceRule describes both: the call signature (total: number): number and the property label: string. Line 15 prints 10 off: 40.The table below compares the two ways to write a function's type. Read each row as a question, then each way's answer.
| Question | Function type expression | Call signature |
|---|---|---|
| How do you write it? | (total: number) => number | { (total: number): number }, inside an object type. |
| What sits between the brackets and the return type? | An arrow, =>. | A colon, :. |
| Can it list properties too? | No. It describes only the call. | Yes, like label: string next to the call. |
| When do you use it? | Most of the time: callbacks, event handlers, and props. | When the function also carries properties, like a label. |
Contextual typing: types from where a function goes
You've seen callbacks with no types written on their parameters, like (price) => …, and TypeScript still knew price was a number. When you write a function right where a function type is expected, TypeScript takes the parameter types from that type. This is called contextual typing: the place the function goes, its context, decides the types. It works with the browser's and JavaScript's own functions too:
const prices = [3, 4, 5];const labels = prices.map((price) => "$" + price.toFixed(2));// ^?console.log(labels.join(", "));Line 1 makes an array of numbers. Line 2 passes a callback to map (the map, filter & reduce lesson covers it). The query on line 3 shows (parameter) price: number: TypeScript knows map passes each element of a number array. So price.toFixed(2) is checked, and line 4 prints $3.00, $4.00, $5.00.
prices holds numbers, and map passes each one to its callback as a number. So price is a number, and you didn't write the type.Take the same arrow function out of its context, and there's nothing to take the type from:
const showPrice = (price) => "$" + price.toFixed(2);Line 1 stores the arrow function in a plain variable. Nothing says what price will be, so TypeScript says Parameter 'price' implicitly has an 'any' type. any is the type that turns checking off, and strict mode won't let it slip in unnoticed. Fix it by writing (price: number), or by giving the variable a function type, as format: PriceFormatter did.
Event listeners use contextual typing too. The browser's types know which kind of event each name sends (the Introduction to events lesson covers listeners). Type a different event name and watch the type of event change:
const button = document.createElement("button");button.addEventListener("keydown", (event) => { // ^? console.log(event.key);});"keydown"KeyboardEventNo errorsA keydown listener gets a KeyboardEvent, which has a key property: the key that was pressed. Line 4 is fine.
With keydown or keyup, event is a KeyboardEvent, so line 4 can read event.key. With click it's a PointerEvent, and with input a plain Event; neither has a key, so line 4 gets an error. A misspelled name like keydwon gets no error of its own, because any name is allowed; TypeScript just falls back to Event.
A search box that calls you back
A real page often has a small component that reports what the user did through a callback. Here is a search box: you pass it a function, and it calls that function with the cleaned-up search text each time the user types.
type SearchHandler = (query: string) => void; function createSearchBox(onSearch: SearchHandler) { const input = document.createElement("input"); input.addEventListener("input", () => onSearch(input.value.trim())); return input;} const searches: string[] = [];const box = createSearchBox((query) => searches.push(query));// ^?box.value = " tea ";box.dispatchEvent(new Event("input"));console.log(searches.join(", "));Line 1 names the handler's type: it gets the search text and gives back nothing to use. Line 3 says createSearchBox takes one. Lines 4 and 5 make an <input> and, on every input event, call onSearch with the text, trimmed of spaces. Line 10 passes a handler. The query on line 11 shows query is a string, from contextual typing. The handler returns push's number, and void lets that go. Lines 12 and 13 act like a user typing " tea ", and line 14 prints tea.
The search box's author wrote one type, on line 1. Everyone who uses the box gets their query typed for free, and gets an error if their handler expects anything but a string.
const format: (price: number) => string = (price) => price * 2;const onSave: (count: number) => void = (count) => count * 2;const lineTotal: (price: number, quantity: number) => number = (price) => price;const withTax: (price: number) => number = (price: number, tax: number) => price + tax;const greet: (name: string) => void = (name: number) => {};const onPress: (count: number) => void = (clicks) => console.log(clicks);[3, 4].forEach((price) => price.toUpperCase());["Tea", "Cake"].map((name) => name.toUpperCase());
Sort each line by what TypeScript says about it. Each line is checked on its own.
Common mix-ups
TypeScript has a built-in type called Function. It sounds like the right type for a callback, but it lets almost anything through:
function runLater(callback: Function) { return callback(1, 2, 3);} const result = runLater(() => "done");// ^?console.log(result.toFixed());Line 1 types callback as Function. Line 2 calls it with three numbers, which nobody checks. The query on line 6 shows const result: any: a call to a Function gives back any. So line 7's result.toFixed() gets no error, and it throws a TypeError when it runs, because result is the text "done". Write the real type instead, like () => string.
- "
(number) => stringis a function that takes a number." No. It's a parameter namednumber, with no type. Write(price: number) => string. - "A callback must take every parameter its type lists." No. It may take fewer. The extra arguments are passed and ignored.
- "Parameter names in a callback must match the type's." No. Only the position and the type count:
(clicks) => …fits(count: number) => void. - "A callback for a
voidtype must not return anything." No. The caller ignores what comes back, so(price) => saved.push(price)is fine. Only a function written with: voidmust not return a value. - "Mark a callback parameter optional if some callbacks don't need it." No.
?means you might not pass it, and every callback then has to check forundefined. - "
Functionis a good type for a callback." No. Any call is allowed and the result isany, so mistakes slip through. Write the real function type.
Practice exercises
Each exercise is TypeScript. When you fix code, press Edit & run on the starter code: the errors update as you type, and the console shows what it prints. Try a guess first, then use the hints.
No type is written on name. What type does TypeScript give it? Guess first, then point at name, or press Edit & run and add a // ^? line under it.
const names = ["Tea", "Cake"];const loud = names.map((name) => name.toUpperCase());const names = ["Tea", "Cake"];const loud = names.map((name) => name.toUpperCase());// ^?names is an array of strings, and map passes each element to the callback, so contextual typing gives name the type string. The query on line 3 shows (parameter) name: string.
Two handlers for adding to a cart, one with a mistake. Which line will TypeScript mark?
type OnAdd = (item: string, quantity: number) => void;
const logAdd: OnAdd = (item, quantity) => console.log(quantity + " x " + item);
const badAdd: OnAdd = (item: number) => console.log(item);type OnAdd = (item: string, quantity: number) => void; const logAdd: OnAdd = (item, quantity) => console.log(quantity + " x " + item);const badAdd: OnAdd = (item: number) => console.log(item); // line 4: item is a stringLine 4 says item: number, but whoever calls an OnAdd passes a string first. TypeScript reports Types of parameters 'item' and 'item' are incompatible on line 4.
TypeScript reports three errors: nothing says what listener, count, or total are. Fix all three by writing one type on line 1. What does the fixed code print?
function onCartChange(listener) { listener(3, 12);} onCartChange((count, total) => console.log(count + " items, $" + total));function onCartChange(listener: (count: number, total: number) => void) { listener(3, 12);} onCartChange((count, total) => console.log(count + " items, $" + total));The function type on line 1 says how listener is called: with two numbers, and the result ignored. That fixes line 1, and contextual typing fixes line 5: count and total now get the type number. It prints 3 items, $12.
Line 7 gets an error, and running it throws a TypeError. Fix the callback so it prints each price with two decimals. Once you remove : string, what type does price get?
type PriceCallback = (price: number) => void; function forEachPrice(prices: number[], callback: PriceCallback) { for (const price of prices) callback(price);} forEachPrice([3, 4], (price: string) => console.log(price.toUpperCase()));type PriceCallback = (price: number) => void; function forEachPrice(prices: number[], callback: PriceCallback) { for (const price of prices) callback(price);} forEachPrice([3, 4], (price) => console.log(price.toFixed(2)));// ^?Without : string, the callback takes its parameter type from PriceCallback, so price is a number. Numbers have toFixed, and it prints 3.00 and 4.00.
TypeScript rejects line 10, and running it prints undefined: 15. Make fiveOff a real Discount with the label "5 off". What does your fixed code print?
type Discount = { label: string; (total: number): number;}; function fiveOff(total: number) { return total - 5;} const discount: Discount = fiveOff;console.log(discount.label + ": " + discount(20));type Discount = { label: string; (total: number): number;}; function fiveOff(total: number) { return total - 5;}fiveOff.label = "5 off"; const discount: Discount = fiveOff;console.log(discount.label + ": " + discount(20));Discount has a call signature and a label property. fiveOff already has the right call, and line 9 adds the label, so it fits. Line 12 prints 5 off: 15.
A button takes its handler as a prop, the way components in frameworks like React do. The handler on line 11 doesn't fit. Fix it so each press prints Pressed and the count. What does the second press() print?
type ButtonProps = { label: string; onPress: (count: number) => void }; function makeButton(props: ButtonProps) { let count = 0; return () => { count++; props.onPress(count); };} const press = makeButton({ label: "Add tea", onPress: (label: string) => console.log(label + " pressed") });press();press();type ButtonProps = { label: string; onPress: (count: number) => void }; function makeButton(props: ButtonProps) { let count = 0; return () => { count++; props.onPress(count); };} const press = makeButton({ label: "Add tea", onPress: (count) => console.log("Pressed " + count) });press();press();onPress is called with a number, so the handler's parameter is the count, not a label. With (count) => …, contextual typing makes count a number. The two presses print Pressed 1 and Pressed 2.
Check your understanding
For each question, ask how the function will be called: what it's given, and what happens to what it gives back. 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
(price: number) => stringdescribe?Choose an answer to see the explanation.
Question 2 of 7Which line does TypeScript flag in these three formatters?
Read the code, then predicttype Formatter = (price: number) => string; const a: Formatter = (price) => price.toFixed(2); const b: Formatter = () => "free"; const c: Formatter = (price) => price;Choose an answer to see the explanation.
Question 3 of 7What type does TypeScript give
iteminside this forEach callback?Read the code, then predictconst items = ["Tea", "Cake"]; items.forEach((item) => console.log(item));Choose an answer to see the explanation.
Question 4 of 7forEach's callback type returns
void, andpushreturns a number. What does TypeScript say about line 2?Read the code, then predictconst saved: number[] = []; [3, 4].forEach((price) => saved.push(price));Choose an answer to see the explanation.
Question 5 of 7Why does TypeScript reject line 6?
Read the code, then predicttype PriceRule = { label: string; (total: number): number; }; const rule: PriceRule = (total: number) => total - 10;Choose an answer to see the explanation.
Question 6 of 7With strict on, what does TypeScript make of the type
(number) => string?Choose an answer to see the explanation.
Question 7 of 7A callback type is
(price: number, index: number) => void. Which callback can you pass?Choose an answer to see the explanation.
Key takeaways
- A function type describes a function as a value:
(price: number) => stringtakes a number and gives back a string. - Every parameter in a function type needs a name and a type.
(number) => stringnames a parameternumber. - A type alias gives a function type a name, like
type PriceFormatter = (price: number) => string. - A callback may take fewer parameters than its type lists, but not more, and not different types.
voidin a callback type means the result is ignored, so the callback may return anything.- A call signature,
{ (total: number): number }, describes a function that also has properties. - Contextual typing gives a callback's parameters their types from where you pass it. Avoid the
Functiontype.
Remember the one-liner.
A function type says how a function will be called, and TypeScript checks every function you put in its place.
Coming next: Function overloads. They describe one function that can be called in several different ways, each with its own return type.