Skip to content
aviral gupta

// B4.1 · ~30 min · Beginner

Function type expressions and call signatures

After this lesson you can write and name the type of a function, tell which functions fit it, and type a function with properties or a constructor.

Lesson 1 of 5 in B4 More on functions

Start of the module

You will be able to

  • Write a function type expression with named parameters, and name it with a type alias
  • Use contextual typing, and tell which functions fit a function type
  • Describe a callable object with a call signature, and a constructor with a construct signature
  1. Warm-up · Activity 1 of 7

    Warm-up from JavaScript: functions are values that you can pass to other functions. What does this print?

    function twice(fn, x) {
      return fn(fn(x));
    }
    
    const addThree = (n) => n + 3;
    console.log(twice(addThree, 1));
  2. Predict · Activity 2 of 7

    Predict before you read on. Handler is meant to be a function that takes a string. What does tsc report?

    type Handler = (string) => void;
    
    const shout: Handler = (text) => console.log(text.toUpperCase());
  3. Practice · Activity 3 of 7

    Fill in what goes between the parameter list and the return type, so that Formatter is a function from number to string and the program prints 4.50 EUR.

    type Formatter = (value: number) ____ string;
    
    const euro: Formatter = (value) => value.toFixed(2) + " EUR";
    console.log(euro(4.5));
    type Formatter = (value: number) string;
  4. Practice · Activity 4 of 7

    Four functions are stored as Loggers. Which line does tsc reject?

    type Logger = (message: string, level: number) => void;
    
    const a: Logger = (message, level) => console.log(level, message);
    const b: Logger = (message) => console.log(message);
    const c: Logger = () => console.log("log");
    const d: Logger = (message: string, level: number, time: number) => console.log(time);
  5. Practice · Activity 5 of 7

    Match each type to what it describes.

  6. Brain teaser · Activity 6 of 7

    Brain teaser. Three different functions go into one Check[]: their parameters have other names, or none at all. Does tsc accept it, and what does node main.ts print?

    type Check = (value: number) => boolean;
    
    function isBig(amount: number): boolean {
      return amount > 100;
    }
    
    const checks: Check[] = [isBig, (n) => n % 2 === 0, () => true];
    console.log(checks.map((check) => check(150)).join(" "));
  7. Apply · Activity 7 of 7

    Mini-task. A form checks each field with rules. Write a type Rule with a call signature (it takes the value as a string and returns a boolean) and a message property. Write two rules as function declarations, required and short (at most 10 characters), and give each a message. Then name a function type Report for (field, problems) and write check(field, value, rules, report), which collects the messages of the rules that fail. Pass an arrow function as the Report without annotating its parameters. Check it in the editor, or with npx tsc and node main.ts.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

A checkout with typed steps

A price in cents goes through a list of steps, each a PriceStep. The discount is also a Discount: a function with a label, described by a call signature. addShipping and log get their parameter types from the type they are stored in, and log has fewer parameters than Logger, which is allowed. Run npx tsc, then node main.ts. Then delete the line tenPercentOff.label = "10% off" and run tsc again.

main.ts

// A checkout: a price in cents goes through a list of steps.
type PriceStep = (cents: number) => number;

// A discount is a step with a label: a call signature plus a property.
type Discount = {
  label: string;
  (cents: number): number;
};

function tenPercentOff(cents: number): number {
  return Math.round(cents * 0.9);
}
tenPercentOff.label = "10% off";

const addShipping: PriceStep = (cents) => cents + 490; // cents is a number, from PriceStep

type Logger = (message: string, step: number) => void;
const log: Logger = (message) => console.log("  " + message); // fewer parameters fit

function euro(cents: number): string {
  return (cents / 100).toFixed(2) + " EUR";
}

function checkout(cents: number, steps: PriceStep[], logger: Logger): number {
  let total = cents;
  steps.forEach((step, i) => {
    total = step(total);
    logger("after step " + (i + 1) + ": " + euro(total), i + 1);
  });
  return total;
}

function describe(d: Discount, cents: number): string {
  return d.label + " on " + euro(cents) + " saves " + euro(cents - d(cents));
}

console.log(describe(tenPercentOff, 4999));
console.log("checkout:");
const total = checkout(4999, [tenPercentOff, addShipping], log);
console.log("total: " + euro(total));

Run it with

npx tsc
node main.ts

Output

10% off on 49.99 EUR saves 5.00 EUR
checkout:
  after step 1: 44.99 EUR
  after step 2: 49.89 EUR
total: 49.89 EUR
  • Without the label line, tsc reports the call describe(tenPercentOff, 4999): tenPercentOff is no longer a Discount, though it is still a PriceStep.
  • tenPercentOff is both a Discount and a PriceStep: in the steps array, only its call signature matters.
  • checkout calls logger with two arguments; log uses only the first, and JavaScript ignores the second.
  • forEach is typed the same way: its callback (step, i) gets its types from the array, as in B2.2.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Exercises

Exercise 1 of 2

Name the function type

countWhere counts the items for which test returns true, and isEven is one such test. tsc reports error TS7006 for test and for n. Write an exported type alias NumberTest for a function that takes a number and returns a boolean, and use it for the parameter test and for isEven, so that n gets its type from NumberTest. Give countWhere the return type number. Run the tests: the first one is the type-check.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    A function type is written like an arrow function: (parameter: type) => return type.

  2. Hint 2

    export type NumberTest = (value: number) => boolean; then test: NumberTest.

  3. Hint 3

    For isEven, annotate the const, not the arrow function: export const isEven: NumberTest = (n) => n % 2 === 0;

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export type NumberTest = (value: number) => boolean;

export function countWhere(items: number[], test: NumberTest): number {
  let count = 0;
  for (const item of items) {
    if (test(item)) count++;
  }
  return count;
}

export const isEven: NumberTest = (n) => n % 2 === 0;
Run it on your computer

Install TypeScript 7.0 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.ts

export function countWhere(items: number[], test) {
  let count = 0;
  for (const item of items) {
    if (test(item)) count++;
  }
  return count;
}

export const isEven = (n) => n % 2 === 0;

main.test.ts

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {countWhere, isEven} from './main.ts';

test('isEven tells even numbers from odd ones', () => {
  assert.equal(isEven(4), true, 'isEven(4) should be true');
  assert.equal(isEven(7), false, 'isEven(7) should be false');
});

test('countWhere counts the even numbers in [1, 2, 3, 4]', () => {
  assert.equal(countWhere([1, 2, 3, 4], isEven), 2, 'countWhere([1, 2, 3, 4], isEven) should be 2');
});

test('countWhere works with an arrow function as the test', () => {
  assert.equal(countWhere([5, 12, 20], (n) => n > 10), 2, 'countWhere([5, 12, 20], (n) => n > 10) should be 2');
  assert.equal(countWhere([], isEven), 0, 'countWhere of an empty list should be 0');
});

package.json

{
  "type": "module"
}

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "lib": [
      "esnext",
      "dom"
    ],
    "types": [],
    "strict": true,
    "noEmit": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "**/*.test.ts"
  ]
}

npx tsc needs TypeScript in the folder: run npm install --save-dev typescript there once. tsc checks the types; Node.js runs main.ts by removing them.

Run the program:

npx tsc
node main.ts

Run the checks (needs learnrun.js in the same folder):

npx tsc
node --test
Download learnrun.js

Exercise 2 of 2

A tally with a total

A Tally is a function that adds an amount and returns the new total, and it also has a total property. makeTally keeps the total in a local variable instead, so tsc reports error TS2741: Property 'total' is missing in type '(amount: number) => number' but required in type 'Tally'. Keep the Tally type as it is. Change makeTally so that the function it returns has a total property that starts at 0 and grows with every call.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    A function in JavaScript can have properties. TypeScript lets you add one to a const function right after it is declared.

  2. Hint 2

    Write add.total = 0; after the arrow function and before return add;.

  3. Hint 3

    Inside add, use add.total instead of the local variable: add.total += amount; return add.total;

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export type Tally = {
  total: number;
  (amount: number): number;
};

export function makeTally(): Tally {
  const add = (amount: number): number => {
    add.total += amount;
    return add.total;
  };
  add.total = 0;
  return add;
}
Run it on your computer

Install TypeScript 7.0 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.ts

export type Tally = {
  total: number;
  (amount: number): number;
};

export function makeTally(): Tally {
  let total = 0;
  const add = (amount: number): number => {
    total += amount;
    return total;
  };
  return add;
}

main.test.ts

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {makeTally} from './main.ts';

test('a new tally starts at 0', () => {
  const tally = makeTally();
  assert.equal(tally.total, 0, `a new tally's total is ${tally.total}`);
});

test('calling the tally adds to its total', () => {
  const tally = makeTally();
  tally(5);
  const result = tally(3);
  assert.equal(result, 8, `tally(5) then tally(3) returned ${result}`);
  assert.equal(tally.total, 8, `after tally(5) and tally(3) the total is ${tally.total}`);
});

test('two tallies count separately', () => {
  const a = makeTally();
  const b = makeTally();
  a(10);
  assert.equal(b.total, 0, `after a(10), b's total is ${b.total}`);
});

package.json

{
  "type": "module"
}

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "lib": [
      "esnext",
      "dom"
    ],
    "types": [],
    "strict": true,
    "noEmit": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "**/*.test.ts"
  ]
}

npx tsc needs TypeScript in the folder: run npm install --save-dev typescript there once. tsc checks the types; Node.js runs main.ts by removing them.

Run the program:

npx tsc
node main.ts

Run the checks (needs learnrun.js in the same folder):

npx tsc
node --test
Download learnrun.js

Common mistakes

A parameter type without a parameter name

type Handler = (string) => void;

const shout: Handler = (text) => console.log(text.toUpperCase());
shout("hello");

What tsc or Node.js prints

main.ts(1,17): error TS7051: Parameter has a name but no type. Did you mean 'arg0: string'?

Why, and the fix

In a function type every parameter needs a name, so string is read as the name of a parameter of type any. Write type Handler = (text: string) => void. The name is only for reading: a function whose parameter has another name still fits.

An arrow inside an object type

type Discount = {
  label: string;
  (cents: number) => number;
};

function tenOff(cents: number): number {
  return Math.round(cents * 0.9);
}
tenOff.label = "10% off";

const d: Discount = tenOff;
console.log(d.label, d(2000));

What tsc or Node.js prints

main.ts(3,19): error TS1005: ':' expected.

Why, and the fix

In an object type, a call signature puts a colon between the parameter list and the return type: (cents: number): number;. The arrow belongs to a function type expression on its own, such as type PriceStep = (cents: number) => number.

A callback that needs an argument it never gets

type OnSave = (name: string) => void;

const onSave: OnSave = (name: string, when: number) => console.log(name, when);
onSave("report.pdf");

What tsc or Node.js prints

main.ts(3,7): error TS2322: Type '(name: string, when: number) => void' is not assignable to type 'OnSave'.

Why, and the fix

Whoever calls an OnSave passes one argument, so when would always be undefined; the second line of the message says Target signature provides too few arguments. Either drop the parameter, or, if the caller really has a time to pass, add it to the type: type OnSave = (name: string, when: number) => void.

TypeScript in the browser: the TypeScript 6.0.3 compiler, Apache-2.0, then your browser’s own engine. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

A function type looks like an arrow function

Functions are values: you store them, pass them and put them in arrays, so they need types too. A function type expression is written like an arrow function: (a: string) => void is a function that takes one string and returns nothing useful; (value: number) => string takes a number and returns a string. Name one with a type alias, type Formatter = (value: number) => string, and use the name for parameters, variables and arrays: steps: PriceStep[]. Every parameter needs a name. (string) => void declares a parameter named string with no type, which strict reports as error TS7051: Parameter has a name but no type. Did you mean 'arg0: string'? Which name you choose does not matter.

Contextual typing, and which functions fit

A function written where a function type is expected takes its parameter types from that type. In const euro: Formatter = (value) => value.toFixed(2), value is a number without an annotation, as for callbacks in B2.2, and the body is checked: returning value * 100 is error TS2322: Type 'number' is not assignable to type 'string'. A function with fewer parameters fits: (message) => … is a valid (message: string, level: number) => void, because JavaScript ignores extra arguments. A function that needs a parameter the type never passes does not fit: Target signature provides too few arguments. Callers must pass what the type says: two arguments for one parameter is error TS2554: Expected 1 arguments, but got 2.

Call signatures and construct signatures

In JavaScript a function can have properties too. A function type expression cannot describe them, but a call signature in an object type can: type Discount = { label: string; (cents: number): number }. Inside an object type the return type follows a colon; writing (cents: number) => number there is error TS1005: ':' expected. TypeScript lets you add properties to a function after declaring it: tenOff.label = "10% off" makes tenOff a Discount, and without that line tsc reports error TS2741: Property 'label' is missing. With new in front, it is a construct signature: { new (text: string): Date } accepts Date itself. Calling it without new is error TS2348: … Did you mean to include 'new'?

Sources

Last reviewed October 4, 2026