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.
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:
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));13 42The 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:
type MathOperation = (a: number, b: number) => number;
const join: MathOperation = (a, b) => `${a}${b}`;error TS2322: Type 'string' is not assignable to type 'number'.Typing callbacks#
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));[ '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:
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);[ 10, 20, 30 ]Functions that return functions#
function makeMultiplier(factor: number): (n: number) => number {
return (n) => n * factor;
}
const triple = makeMultiplier(3);
console.log(triple(14));42Call signatures#
If a function also has properties, describe it with an object type containing a call signature (params): returnType:
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());2
1Similarly, 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:
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 undefinederror 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:
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));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
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)]));[ 'This field is required', 'Must be at least 3 characters' ]
[ 'Must be at least 3 characters' ]
[]Key takeaway#
Finished this lesson?