Skip to content
Laviyx Learn

Try

TypeScript course menu

TypeScript Function Types, Callbacks and Overloads

Learn to describe functions as types in TypeScript: function type expressions, typing callbacks and higher-order functions, call signatures, void return types for callbacks, the this parameter, and function overloads for different argument combinations.

Intermediate8 min readUpdated
On this page

Functions are values in JavaScript: they are stored in variables, passed as callbacks and returned from other functions. TypeScript can describe a function's type so all of these are checked.

Function type expressions#

A function type looks like an arrow function with types: (parameters) => returnType:

TypeScript
type MathOperation = (a: number, b: number) => number;

const add: MathOperation = (a, b) => a + b;
const multiply: MathOperation = (a, b) => a * b;

function calculate(op: MathOperation, x: number, y: number): number {
  return op(x, y);
}

console.log(calculate(add, 6, 7), calculate(multiply, 6, 7));
Output
13 42

The parameter names in the type (a, b) are only labels; implementations can use any names. Because add is declared with MathOperation, its parameters are typed automatically.

A function that does not match is rejected:

TypeScript
type MathOperation = (a: number, b: number) => number;
const join: MathOperation = (a, b) => `${a}${b}`;
Compiler error
error TS2322: Type 'string' is not assignable to type 'number'.

Typing callbacks#

TypeScript
type Product = { name: string; price: number };

function filterProducts(products: Product[], test: (p: Product) => boolean): Product[] {
  const result: Product[] = [];
  for (const p of products) if (test(p)) result.push(p);
  return result;
}

const products: Product[] = [
  { name: "Pen", price: 20 },
  { name: "Bag", price: 1500 },
];

console.log(filterProducts(products, (p) => p.price < 100).map((p) => p.name));
Output
[ 'Pen' ]

void callbacks may return values#

A callback type that returns void means "the caller ignores the result", so functions that return something are still accepted. That is why you can pass push (which returns a number) to forEach:

TypeScript
const source = [1, 2, 3];
const target: number[] = [];

source.forEach((n) => target.push(n * 10));   // push returns a number; forEach ignores it
console.log(target);
Output
[ 10, 20, 30 ]

Functions that return functions#

TypeScript
function makeMultiplier(factor: number): (n: number) => number {
  return (n) => n * factor;
}

const triple = makeMultiplier(3);
console.log(triple(14));
Output
42

Call signatures#

If a function also has properties, describe it with an object type containing a call signature (params): returnType:

TypeScript
type Counter = {
  (): number;          // calling it returns a number
  reset(): void;       // and it has a reset method
  count: number;
};

function createCounter(): Counter {
  const counter = (() => ++counter.count) as Counter;
  counter.count = 0;
  counter.reset = () => {
    counter.count = 0;
  };
  return counter;
}

const next = createCounter();
next();
next();
console.log(next.count);
next.reset();
console.log(next());
Output
2
1

Similarly, new (...args) => T describes a constructor (something you call with new).

The this parameter#

A fake first parameter named this declares what this must be inside a function. It is removed from the compiled output:

TypeScript
type User = { name: string };

function greet(this: User, greeting: string) {
  return `${greeting}, ${this.name}`;
}

const asha = { name: "Asha", greet };
asha.greet("Hello");       // OK: called on a User
greet("Hi");               // error: this would be undefined
Compiler error
error TS2684: The 'this' context of type 'void' is not assignable to method's 'this' of type 'User'.

This catches the "lost this" bug from this, call, apply and bind.

Overloads#

Sometimes a function accepts different argument combinations and returns different types depending on them. Overloads list each allowed form, followed by one implementation that handles them all:

TypeScript
function parse(value: string): number;
function parse(value: string[]): number[];
function parse(value: string | string[]): number | number[] {
  return Array.isArray(value) ? value.map(Number) : Number(value);
}

const one = parse("42");            // type: number
const many = parse(["1", "2"]);     // type: number[]

console.log(one + 1, many.map((n) => n * 2));
Output
43 [ 2, 4 ]
  • The first two lines are overload signatures: what callers can see.
  • The last signature is the implementation: it must be compatible with all the overloads, and callers cannot use it directly.

Without overloads, the return type would be number | number[] for every call, and callers would have to narrow it themselves. Prefer a union parameter or generics when they express the same thing more simply; use overloads when the return type depends on the argument types.

Practice#

Practice exercise

Define type Validator = (value: string) => string | null (returning an error message or null). Write required, minLength(n) (which returns a Validator) and validate(value: string, validators: Validator[]): string[] that returns all error messages. Test with "" and "ab".

Show solution
TypeScript
type Validator = (value: string) => string | null;

const required: Validator = (value) => (value.trim() === "" ? "This field is required" : null);

const minLength =
  (n: number): Validator =>
  (value) =>
    value.length < n ? `Must be at least ${n} characters` : null;

function validate(value: string, validators: Validator[]): string[] {
  return validators.map((v) => v(value)).filter((msg): msg is string => msg !== null);
}

console.log(validate("", [required, minLength(3)]));
console.log(validate("ab", [required, minLength(3)]));
console.log(validate("asha", [required, minLength(3)]));
Output
[ 'This field is required', 'Must be at least 3 characters' ]
[ 'Must be at least 3 characters' ]
[]

Key takeaway#

Finished this lesson?