Loading
All articles
Programming 16 min read

TypeScript Function Types: A Visual, Hands-On Guide

Learn TypeScript function types visually: how to type a variable that holds a function, why parameter names don't matter, writing types only once, type aliases, what fits a function type, callbacks, void, why to avoid Function, what types can't catch, and exercises.

nbkhope
On this page

So far in this series you've typed what goes into a function and what comes out of it. There's one more piece. In JavaScript, a function is a value: you can store it in a variable, pass it to another function, or return it. So a function needs a type of its own.

In this guide you'll learn to read and write a function type, then see why parameter names don't matter, how to avoid writing the types twice, and how to give a function type a name. You'll also test which functions fit a type and which don't, use function types for callbacks, and meet a few traps, including one line of code that compiles and gives the wrong answer. Exercises wrap it up.

All outputs below come from real runs with TypeScript 7.0 and Node.js 26 on Windows. Save each example as main.ts, then run npx tsc main.ts and node main.js.

Functions are values

A variable can have a type annotation, which the compiler then enforces:

let myNumber: number = 48;
myNumber = "48";
main.ts(2,1): error TS2322: Type 'string' is not assignable to type 'number'.

A variable can also hold an anonymous function. Here, TypeScript infers the variable's type from the function:

1let myFunction = function (text: string, count: number): string {
2  return `The string is ${text} and the number is ${count}`;
3};
4
5console.log(myFunction("Ana", 48));
6myFunction = 48;
main.ts(6,1): error TS2322: Type 'number' is not assignable to type '(text: string, count: number) => string'.
The string is Ana and the number is 48

The error message shows you the type of myFunction. It isn't function. It's the whole signature: (text: string, count: number) => string.

Reading a function type

That's a function type. It looks like an arrow function, but it describes a function and contains no code:

The parts of a function type. In (text: string, count: number) => string, the parentheses hold the parameters, each with a name and a type. The arrow separates what goes in from what comes out. The type after the arrow is the return type. It's only a description, so there's no code in it

Read it as "a function that takes a string and a number, and returns a string". To annotate the variable yourself, put the function type where you'd normally put number:

1let myFunction: (text: string, count: number) => string =
2  function (text: string, count: number): string {
3    return `The string is ${text} and the number is ${count}`;
4  };
5
6console.log(myFunction("Ana", 48));
The string is Ana and the number is 48

Names don't matter, types do

The names in a function type are only labels for people reading it. They don't have to match the real function's parameters. What must match is the types, in order:

1let myFunction: (label: string, amount: number) => string =
2  function (text: string, count: number): string {
3    return `The string is ${text} and the number is ${count}`;
4  };
5
6console.log(myFunction("Ana", 48));
The string is Ana and the number is 48

Change the type of one parameter, though, and the function no longer fits:

1let myFunction: (label: string, amount: number) => string =
2  function (text: number, count: number): string {
3    return `The first is ${text} and the second is ${count}`;
4  };
5
6console.log(myFunction("Ana", 48));
main.ts(1,5): error TS2322: Type '(text: number, count: number) => string' is not assignable to type '(label: string, amount: number) => string'.
  Types of parameters 'text' and 'label' are incompatible.
    Type 'string' is not assignable to type 'number'.

Writing the types only once

That last declaration says everything twice, and the line gets long. You don't need both sides. On its own, a function with no annotations is an error:

1let myFunction = function (text, count) {
2  return `The string is ${text} and the number is ${count}`;
3};
4
5console.log(myFunction("Ana", 48));
main.ts(1,28): error TS7006: Parameter 'text' implicitly has an 'any' type.
main.ts(1,34): error TS7006: Parameter 'count' implicitly has an 'any' type.

Put the type on the variable, and the same function compiles. TypeScript takes the types from the left side and applies them to the right side, which is called contextual typing:

1let myFunction: (text: string, count: number) => string = function (text, count) {
2  return `${text.toUpperCase()} and ${count.toFixed(1)}`;
3};
4
5console.log(myFunction("Ana", 48));
ANA and 48.0

text is a string and count is a number, so toUpperCase and toFixed are both allowed. The return value is still checked as well:

1let myFunction: (text: string) => string = function (text) {
2  return text.length;
3};
4
5console.log(myFunction("Ana"));
main.ts(1,5): error TS2322: Type '(text: string) => number' is not assignable to type '(text: string) => string'.
  Type 'number' is not assignable to type 'string'.

So there are two good options: annotate the function and let the variable be inferred, or annotate the variable and let the function be inferred.

Giving a function type a name

A long function type is hard to read in the middle of a line. As with objects, type gives it a name you can use anywhere:

1type Operation = (first: number, second: number) => number;
2
3const add: Operation = (first, second) => first + second;
4const multiply: Operation = (first, second) => first * second;
5
6console.log(add(3, 4));
7console.log(multiply(3, 4));
7
12

The name also says what the functions are for, and you can reuse it. Here's a list of them:

1type Operation = (first: number, second: number) => number;
2
3const operations: Operation[] = [
4  (first, second) => first + second,
5  (first, second) => first * second,
6];
7
8for (const operation of operations) {
9  console.log(operation(3, 4));
10}
7
12

What fits a function type

How strict is the match? We tried eight functions against the same type:

1type Formatter = (text: string, count: number) => string;
2
3const differentNames: Formatter = (label: string, amount: number) => "ok";
4const fewerParameters: Formatter = (text: string) => text;
5const noParameters: Formatter = () => "ok";
6const extraOptional: Formatter = (text: string, count: number, loud?: boolean) => text;
7const extraRequired: Formatter = (text: string, count: number, loud: boolean) => text;
8const wrongParameter: Formatter = (text: number, count: number) => "ok";
9const wrongReturn: Formatter = (text: string, count: number) => count;
10const swappedOrder: Formatter = (count: number, text: string) => text;
What fits (text: string, count: number) => string? Four functions fit: different names, because only the types matter; fewer parameters, because a function may ignore arguments; no parameters, for the same reason; and an extra optional parameter, because it can be left out. Four don't fit: an extra required parameter, because no caller would pass it; a wrong parameter type; a wrong return type; and a swapped order, because order matters. Each of the four is reported as error TS2322

Most of those results make sense right away. The surprising one is that a function with fewer parameters fits. The caller will pass two arguments, and a function is free to ignore the ones it doesn't need. That's why you can write numbers.map((value) => value * 2), even though map passes each callback three arguments. A required extra parameter is different, because no caller would ever pass it.

Colon or arrow?

The most common beginner mistake is mixing up where the return type goes, because the same idea takes three forms:

Colon or arrow? In a function declaration, function toLabel(value: number): string, a colon comes before the return type. In an arrow function, (value: number): string => ..., it's also a colon, and the arrow starts the code. In a function type, (value: number) => string, the arrow comes before the return type, and there's no code

A colon inside a type gives you this:

let toLabel: (value: number): string = (value) => `#${value}`;
main.ts(1,29): error TS1005: '=>' expected.

The second mistake is writing only the types in a function type. Each parameter needs a name, so TypeScript reads string and number here as parameter names:

1let myFunction: (string, number) => string = function (text, count) {
2  return `${text} ${count}`;
3};
4
5console.log(myFunction("Ana", 48));
main.ts(1,18): error TS7051: Parameter has a name but no type. Did you mean 'arg0: string'?
main.ts(1,26): error TS7051: Parameter has a name but no type. Did you mean 'arg1: number'?

Callbacks: passing a function

The most common use of a function type is a parameter that takes a function, known as a callback:

1function applyTwice(value: number, action: (current: number) => number): number {
2  return action(action(value));
3}
4
5console.log(applyTwice(3, (current) => current * 2));
6console.log(applyTwice(3, (current) => current + 10));
12
23

current needs no annotation, because applyTwice already says what it is. The callback's result is checked too:

1function applyTwice(value: number, action: (current: number) => number): number {
2  return action(action(value));
3}
4
5console.log(applyTwice(3, (current) => `${current}`));
main.ts(5,40): error TS2322: Type 'string' is not assignable to type 'number'.

JavaScript's built-in functions are typed the same way. sort expects a callback that returns a number, and a very common bug is returning a boolean:

const numbers = [3, 1, 2];
numbers.sort((first, second) => first > second);
console.log(numbers);
main.ts(2,14): error TS2345: Argument of type '(first: number, second: number) => boolean' is not assignable to parameter of type '(a: number, b: number) => number'.
  Type 'boolean' is not assignable to type 'number'.
[ 3, 1, 2 ]

The compiler found the bug, and the output confirms it: the array wasn't sorted.

Returning a function

A function type also works as a return type. Careful with the two arrows here: the colon starts the return type, and the return type is itself a function type:

1function makeMultiplier(factor: number): (value: number) => number {
2  return (value) => value * factor;
3}
4
5const double = makeMultiplier(2);
6const triple = makeMultiplier(3);
7
8console.log(double(5));
9console.log(triple(5));
10
15

Optional callbacks

A callback can be optional, like any other parameter. It might then be undefined, so you can't just call it:

1function save(text: string, onDone?: () => void): void {
2  console.log(`Saved ${text}`);
3  onDone();
4}
5
6save("notes");
main.ts(3,3): error TS2722: Cannot invoke an object which is possibly 'undefined'.

The ?.() syntax calls it only if it's there:

1function save(text: string, onDone?: () => void): void {
2  console.log(`Saved ${text}`);
3  onDone?.();
4}
5
6save("notes");
7save("report", () => console.log("Done"));
Saved notes
Saved report
Done

Functions inside objects

An object type can describe its functions too, in either of two forms:

1type Student = {
2  name: string;
3  describe: () => string;
4  addPoints(points: number): number;
5};
6
7const student: Student = {
8  name: "Ana",
9  describe: () => "Ana is a student",
10  addPoints: (points) => points + 1,
11};
12
13console.log(student.describe());
14console.log(student.addPoints(9));
Ana is a student
10

describe uses the arrow form you've seen in this guide. addPoints uses the method form, with a colon before the return type. Either way, the object must provide a function that fits.

void in a function type

Here's a rule that looks like a contradiction. In the return types guide, a function declared with : void couldn't return a value. In a function type, it can:

1type Logger = (message: string) => void;
2
3const logLength: Logger = (message) => message.length;
4
5console.log(logLength("hello"));
6
7const numbers: number[] = [];
8[1, 2].forEach((value) => numbers.push(value));
9console.log(numbers);
5
[ 1, 2 ]

This is on purpose. void in a function type means "the caller will ignore the result", not "the function must return nothing". Without this rule, the forEach line wouldn't compile, because push returns a number and forEach expects a void callback.

Avoid the Function type

TypeScript has a built-in type called Function, and it's tempting. It accepts any function, and it also turns off the checks you came here for:

1const add: Function = (first: number, second: number) => first + second;
2
3console.log(add("3"));
4console.log(add("3", 5, true).toUpperCase());
3undefined
35

This compiled with no errors. The wrong types, the wrong number of arguments and the method on the result were all accepted, and you're back to the bug from the first guide in this series. Write the real signature instead, even if it's long, and give it a name with type.

What function types can't catch

Remember that fewer parameters fit, and that map passes three arguments? That rule has a famous cost. Try turning some text into numbers:

const values = ["1", "2", "3"].map(parseInt);
console.log(values);
[ 1, NaN, NaN ]

This compiles with no errors, and the answer is wrong. map calls its callback with each value and its index, and parseInt has an optional second parameter: the base of the number. So the calls are parseInt("1", 0), parseInt("2", 1) and parseInt("3", 2). Each one is valid according to the types, and the last two don't mean what you wanted.

A function type checks that the arguments fit. It can't check that they mean what you think. Passing just the argument you intend fixes it:

const values = ["1", "2", "3"].map((text) => parseInt(text));
console.log(values);
[ 1, 2, 3 ]

Your turn: practice

Try these before peeking at the answers.

Exercise 1: What fits? (warm-up). Which of these lines does TypeScript reject, and why?

1type Compare = (first: number, second: number) => boolean;
2
3const isBigger: Compare = (first, second) => first > second;
4const isPositive: Compare = (first) => first > 0;
5const total: Compare = (first, second) => first + second;
6const isLonger: Compare = (first: string, second: string) => first.length > second.length;

Exercise 2: Greetings. Define a type Greeter for a function that takes a name and returns text. Create two greeters, greetFormal and greetCasual, and print the result of each for "Ana".

Exercise 3: Repeat. Write repeatAction(times: number, action: (round: number) => void): void, which calls action once for each round, starting at 1. Use it to print Round 1, Round 2 and Round 3.

Exercise 4: Counter. Write makeCounter(start: number) with an explicit return type. It returns a function with no parameters that gives the next number each time you call it. Starting at 5, the first two calls give 6 and 7.

Exercise 5: Apply them all (challenge). Using the Operation type from this guide, write applyAll(first: number, second: number, operations: Operation[]): number[], which returns the result of each operation. Try it with 3 and 4, and with addition and multiplication.

Quick quiz

  1. How do you read the type (text: string) => number?
  2. Do the parameter names in a function type have to match the function's?
  3. Why does function (text, count) compile when the variable already has a function type?
  4. Why does a function with fewer parameters fit a function type?
  5. What's wrong with the type (string, number) => string?
  6. Before the return type, do you write a colon or an arrow in a function type? And in an arrow function?
  7. What does void mean in a function type?
  8. Why should you avoid the Function type?
  9. Why does ["1", "2", "3"].map(parseInt) compile?

Answers

Exercise 1: The first two lines are fine, and isPositive simply ignores the second argument. total returns a number instead of a boolean, and isLonger takes strings instead of numbers:

1main.ts(5,43): error TS2322: Type 'number' is not assignable to type 'boolean'.
2main.ts(6,7): error TS2322: Type '(first: string, second: string) => boolean' is not assignable to type 'Compare'.
3  Types of parameters 'first' and 'first' are incompatible.
4    Type 'number' is not assignable to type 'string'.

Exercise 2: The type gives name its type in both functions:

1type Greeter = (name: string) => string;
2
3const greetFormal: Greeter = (name) => `Good morning, ${name}.`;
4const greetCasual: Greeter = (name) => `Hi, ${name}!`;
5
6console.log(greetFormal("Ana"));
7console.log(greetCasual("Ana"));
Good morning, Ana.
Hi, Ana!

Exercise 3: A plain for loop calls the callback:

1function repeatAction(times: number, action: (round: number) => void): void {
2  for (let round = 1; round <= times; round += 1) {
3    action(round);
4  }
5}
6
7repeatAction(3, (round) => console.log(`Round ${round}`));
Round 1
Round 2
Round 3

Exercise 4: The return type is () => number. The inner function remembers current between calls:

1function makeCounter(start: number): () => number {
2  let current = start;
3  return () => {
4    current += 1;
5    return current;
6  };
7}
8
9const next = makeCounter(5);
10
11console.log(next());
12console.log(next());
6
7

Exercise 5: map runs each operation:

1type Operation = (first: number, second: number) => number;
2
3function applyAll(first: number, second: number, operations: Operation[]): number[] {
4  return operations.map((operation) => operation(first, second));
5}
6
7const add: Operation = (first, second) => first + second;
8const multiply: Operation = (first, second) => first * second;
9
10console.log(applyAll(3, 4, [add, multiply]));
[ 7, 12 ]

Quiz: (1) A function that takes a string and returns a number. (2) No. Only the types and their order matter. (3) Contextual typing: TypeScript takes the types from the variable. (4) The function is free to ignore arguments it doesn't need. (5) It has no parameter names, so string and number are read as names with no types. (6) An arrow in a function type, and a colon in an arrow function. (7) The caller will ignore the result. (8) It accepts any arguments and returns any, which turns the checks off. (9) parseInt takes a string and an optional number, and that fits what map passes, even though the index isn't the number base you wanted.

Cheat sheet

I want to…Write
Type a variable that holds a functionlet toLabel: (value: number) => string
Write the types only oncelet toLabel: (value: number) => string = (value) => …
Name a function typetype Operation = (first: number, second: number) => number;
Accept a callbackfunction run(action: (value: number) => number)
Accept an optional callbackonDone?: () => void
Call an optional callbackonDone?.();
Return a functionfunction makeCounter(start: number): () => number
Keep a list of functionsconst operations: Operation[] = [ … ];
Put a function in an object typedescribe: () => string;
Say a callback's result is ignored(message: string) => void

Wrapping up

A function type describes a function the same way number describes a number:

  • A function type is written (text: string) => string, and it contains no code.
  • Parameter names are labels. Types and order are what must match.
  • Write the types on one side. Contextual typing fills in the other.
  • type gives a long function type a name.
  • A function with fewer parameters fits, because it can ignore arguments. A required extra parameter doesn't.
  • Use an arrow in a function type, and a colon in a function.
  • Callbacks are function types on parameters, and their results are checked.
  • Use ?.() to call an optional callback.
  • void in a function type means the result is ignored.
  • Avoid Function. Write the real signature.
  • Types can't check meaning, as map(parseInt) shows.

Next up: an introduction to TypeScript classes.

Thanks for reading!

Know someone who would enjoy this? Send it their way.

More articles

Read next

Programming 4 min read

Ruby Blocks and Iterators: Write Your Own

Learn how Ruby methods receive blocks, pass values with yield, handle a missing block, and return an Enumerator. Build and run your own iterator.

nbkhope
Programming 6 min read

C++ std::vector: Size, Capacity, and Safe Growth

Learn how std::vector grows in C++: the difference between size and capacity, when reserve helps, why resize is different, and how growth can invalidate references.

nbkhope
Programming 5 min read

How to Handle Errors in Go: A Hands-On Guide

Learn Go's everyday error pattern with runnable examples: return an error, check it, add context with %w, and recognize a missing file with errors.Is.

nbkhope

Comments

Be kind and specific. Comments with links from new accounts appear after a moderator reviews them.