cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

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.

By the end, you can
  • 01
    Write a function's typeDescribe a function as a value with (price: number) => string, and give it a name with a type alias.
  • 02
    Type callbacks safelyType the functions you pass to other functions, and read the errors when a callback doesn't fit.
  • 03
    Use 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:

A save that calls you backPop out in the code editor (opens in a new tab)JavaScript
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:

The same save, with a function type (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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.
The type (count: number) => void split into three cards. count: number is what the function takes. The arrow comes next. void is what it gives back: nothing for the caller to use.onSaved: (count: number) => void(count: number)=>voidwhat it takes:a number, countwhat it gives back:nothing to use
Read a function type left to right. The brackets hold what the function takes, a number called count. After the => comes what it gives back. Here it's void: nothing the caller uses.
Real-life analogyLeaving your number at the bakery

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: count is a number, and void means 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
The main idea

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, or void for 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.

A named function type for formatting pricesPop out in the code editor (opens in a new tab)TypeScript
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:

Playground: which functions are a PriceFormatter?
A price formatterPop out in the code editor (opens in a new tab)TypeScript
type PriceFormatter = (price: number) => string; const format: PriceFormatter = (price) => "$" + price.toFixed(2);console.log(format(4));
Console output$4.00
What happens
Line 3 stores(price) => "$" + price.toFixed(2)
TypeScriptNo errors
Try it yourself

The function takes a number and returns text, exactly what PriceFormatter describes. TypeScript has nothing to report, and line 4 prints $4.00.

TypeScript checks the code for real, and the output is what line 4 prints. Each button changes only line 3. Reset goes back to Matches.

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:

A function type with a type but no name (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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:

A function that calls back with each pricePop out in the code editor (opens in a new tab)TypeScript
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:

Playground: which callbacks fit PriceCallback?
Prices passed to a callbackPop out in the code editor (opens in a new tab)TypeScript
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));
When it runs0: 3, then 1: 4
What happens
Line 7 passes(price, index) => console.log(index + ": " + price)
TypeScriptNo errors
Try it yourself

The callback takes both parameters. price is a number and index is a number, both worked out from PriceCallback.

TypeScript checks the code for real, and the output is what the code prints. Each button changes only line 7. Reset goes back to Uses both.

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.

forEachPrice calls callback(3, 0) on line 4. The price 3 goes into the callback (price) => console.log(price). The index 0 is passed too, and the callback ignores it.callback(3, 0)30price(price) => console.log(price)the index: passed, and ignoredprints 3
Line 4 always passes two values, like 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.

What a callback may do, compared with its type
When the callback…What TypeScript saysWhy
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 listsAn error: Target signature provides too few arguments.The caller never passes them, so they would be undefined.
Expects a different type, like price: stringAn 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 voidFine.void in a function type means the caller ignores whatever comes back.
Returns a number when the type says stringAn 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:

An optional index in a callback type (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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:

void in a callback type and on a function (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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:

A discount rule with a label (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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.

The function tenOff drawn as a cart holding two cards: round brackets for the call, and label. Calling tenOff(50) gives 40. Reading tenOff.label gives the text 10 off.()labeltenOfftenOff(50)40tenOff.label"10 off"
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.

Function type expressions and call signatures
QuestionFunction type expressionCall 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:

map gives the callback its parameter typePop out in the code editor (opens in a new tab)TypeScript
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.

Three cards joined by arrows. prices: number[], the array. Then (value: number, …), what map passes to its callback. Then price: number, the type your callback's parameter gets.prices.map((price) => "$" + price.toFixed(2))prices: number[](value: number, …)price: numberthe arraywhat map passesyour parameter
TypeScript reads the place the callback goes. 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:

An arrow function with no context (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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:

Playground: type the event name
A listener on a buttonPop out in the code editor (opens in a new tab)TypeScript
const button = document.createElement("button");button.addEventListener("keydown", (event) => {  //                                ^?  console.log(event.key);});
The event name"keydown"
What TypeScript says
Type of eventKeyboardEvent
ErrorsNo errors
Try it yourself

A keydown listener gets a KeyboardEvent, which has a key property: the key that was pressed. Line 4 is fine.

The code is checked by the real TypeScript compiler each time you type. Only the name on line 2 changes. Reset puts keydown back.

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.

A search box with a typed handlerPop out in the code editor (opens in a new tab)TypeScript
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.

Error or fine?
  • 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());
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

TypeScript has a built-in type called Function. It sounds like the right type for a callback, but it lets almost anything through:

A callback typed as FunctionPop out in the code editor (opens in a new tab)TypeScript
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) => string is a function that takes a number." No. It's a parameter named number, 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 void type must not return anything." No. The caller ignores what comes back, so (price) => saved.push(price) is fine. Only a function written with : void must 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 for undefined.
  • "Function is a good type for a callback." No. Any call is allowed and the result is any, 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.

Exercise 1 · Warm-upRead a callback's parameter type

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.

Starter codePop out in the code editor (opens in a new tab)TypeScript
const names = ["Tea", "Cake"];const loud = names.map((name) => name.toUpperCase());

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

    Exercise 2 · Warm-upFind the handler that doesn't fit

    Two handlers for adding to a cart, one with a mistake. Which line will TypeScript mark?

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    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);

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

      Exercise 3 · PracticeWrite a callback's type

      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?

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      function onCartChange(listener) {  listener(3, 12);} onCartChange((count, total) => console.log(count + " items, $" + total));

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

        Exercise 4 · PracticeFix a callback that expects text

        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?

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        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()));

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

          Exercise 5 · ChallengeGive a discount its label

          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?

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          type Discount = {  label: string;  (total: number): number;}; function fiveOff(total: number) {  return total - 5;} const discount: Discount = fiveOff;console.log(discount.label + ": " + discount(20));

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

            Exercise 6 · ChallengeFix a button's onPress prop

            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?

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            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();

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

              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.

              The Function types quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does the type (price: number) => string describe?

                Choose an answer to see the explanation.

              2. Question 2 of 7Which line does TypeScript flag in these three formatters?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type 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.

              3. Question 3 of 7What type does TypeScript give item inside this forEach callback?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                const items = ["Tea", "Cake"];
                items.forEach((item) => console.log(item));

                Choose an answer to see the explanation.

              4. Question 4 of 7forEach's callback type returns void, and push returns a number. What does TypeScript say about line 2?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                const saved: number[] = [];
                [3, 4].forEach((price) => saved.push(price));

                Choose an answer to see the explanation.

              5. Question 5 of 7Why does TypeScript reject line 6?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type PriceRule = {
                  label: string;
                  (total: number): number;
                };
                
                const rule: PriceRule = (total: number) => total - 10;

                Choose an answer to see the explanation.

              6. Question 6 of 7With strict on, what does TypeScript make of the type (number) => string?

                Choose an answer to see the explanation.

              7. 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) => string takes a number and gives back a string.
              • Every parameter in a function type needs a name and a type. (number) => string names a parameter number.
              • 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.
              • void in 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 Function type.

              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.

              CompleteFrontend Clear concepts. Working examples.