Functions
Typing functions in TypeScript: parameter and return types, optional and default parameters, rest parameters, function types, void, and overloads.
Parameter and return types
Everything about functions in JavaScript still applies: declarations, arrow functions, closures, callbacks. TypeScript adds a type to each parameter and, optionally, to the result. The general idea of a signature as a contract is explained in Functions and Abstraction; in TypeScript the checker enforces that contract on every call.
function energyCost(kWh: number, pricePerKWh: number): number {
return kWh * pricePerKWh;
}
const withVat = (amount: number): number => amount * 1.21;
console.log(energyCost(318.4, 0.32).toFixed(2));
console.log(withVat(energyCost(318.4, 0.32)).toFixed(2));
101.89 123.28
In an arrow function the return type goes after the parameter list,
before the =>. JavaScript lets you call a function
with too few or too many arguments: missing ones become
undefined and extra ones are ignored (see
Parameters and arguments). TypeScript does not:
function energyCost(kWh: number, pricePerKWh: number): number {
return kWh * pricePerKWh;
}
console.log(energyCost(318.4));
example.ts(4,13): error TS2554: Expected 2 arguments, but got 1.
function energyCost(kWh: number, pricePerKWh: number): number {
return kWh * pricePerKWh;
}
console.log(energyCost(318.4, 0.32, 0.21));
example.ts(4,37): error TS2554: Expected 2 arguments, but got 3.
In plain JavaScript the first call would have printed
NaN and the second would have quietly ignored the VAT
rate someone meant to pass.
Return types
A
return type annotation
is the type written after a function's parameter list, such as
: number; it states what the function gives back, and
the checker verifies every return against it. Without
one, TypeScript infers the return type from the
return statements, and that inferred type is checked
wherever the result is used:
function ticketLabel(id: string, priority: number) {
return `${id} (P${priority})`;
}
const label = ticketLabel("INC-30412", 2);
const length: number = label;
example.ts(5,7): error TS2322: Type 'string' is not assignable to type 'number'.
So why write return types at all? Because an annotation turns a mistake inside the function into an error at the function, instead of a confusing error at some caller. A classic case is a path that forgets to return:
function tariffFor(hour: number): number {
if (hour >= 7 && hour < 23) {
return 0.32;
} else if (hour >= 0 && hour < 7) {
return 0.25;
}
}
example.ts(1,35): error TS2366: Function lacks ending return statement and return type does not include 'undefined'.
Hour 23 falls through both conditions, and the function would return
undefined. Without the : number,
TypeScript would infer number | undefined and complain
only where a caller does arithmetic with the result. A good rule:
write return types on exported functions and on anything
non-trivial; leave them off short callbacks and small local helpers.
Optional and default parameters
An
optional parameter
is marked with a question mark after its name,
priority?: number; callers may leave it out, and inside
the function its type includes undefined. A
default parameter
(pricePerKWh = 0.32) is optional too, and TypeScript
infers its type from the default value, so it needs no annotation.
function ticketLabel(id: string, priority?: number): string {
if (priority === undefined) {
return `${id} (unprioritized)`;
}
return `${id} (P${priority})`;
}
function energyCost(kWh: number, pricePerKWh = 0.32): number {
return kWh * pricePerKWh;
}
console.log(ticketLabel("INC-30412", 2));
console.log(ticketLabel("INC-30415"));
console.log(energyCost(100).toFixed(2), energyCost(100, 0.25).toFixed(2));
INC-30412 (P2) INC-30415 (unprioritized) 32.00 25.00
Because an optional parameter may be undefined, the
checker insists that you handle that case before using the value as
a number. The if above does exactly that; leave it out
and the code is refused:
function ticketLabel(id: string, priority?: number): string {
return `${id} (P${priority.toFixed(0)})`;
}
example.ts(2,21): error TS18048: 'priority' is possibly 'undefined'.
Optional parameters must come after the required ones; otherwise a caller could not tell which argument is which:
function ticketLabel(priority?: number, id: string): string {
return `${id} (P${priority})`;
}
example.ts(1,41): error TS1016: A required parameter cannot follow an optional parameter.
Rest parameters
A
rest parameter
collects the remaining arguments into an array, so its type is
always an array type such as number[]. Spreading an
array into a call works the same way as in JavaScript, and the
element types are checked:
function average(meterId: string, ...readings: number[]): string {
const total = readings.reduce((sum, r) => sum + r, 0);
return `${meterId}: ${(total / readings.length).toFixed(1)} kWh`;
}
console.log(average("E-1001", 11.2, 9.8, 12.5));
const week = [11.2, 9.8, 12.5, 10.1, 13.4];
console.log(average("E-1002", ...week));
E-1001: 11.2 kWh E-1002: 11.4 kWh
Options objects
For functions with several optional settings, JavaScript code passes
one
options object
and unpacks it with destructuring. In TypeScript you describe that
object with an interface whose properties are optional (marked with
?), and the defaults go in the destructuring pattern:
interface TicketOptions {
priority?: "P1" | "P2" | "P3";
team?: string;
notify?: boolean;
}
function createTicket(
title: string,
{ priority = "P3", team = "Service desk", notify = true }: TicketOptions = {},
): string {
return `[${priority}] ${title} -> ${team}${notify ? " (notified)" : ""}`;
}
console.log(createTicket("Meter display blank"));
console.log(createTicket("Substation South offline", { priority: "P1", team: "NOC" }));
console.log(createTicket("Typo on invoice", { notify: false }));
[P3] Meter display blank -> Service desk (notified) [P1] Substation South offline -> NOC (notified) [P3] Typo on invoice -> Service desk
"P1" | "P2" | "P3" is a union of literal types: only
those three strings are allowed (see
Union, Intersection and Literal Types). The checker also catches a misspelled option name, which in
plain JavaScript would simply be ignored:
function createTicket(title: string, options: { priority?: string } = {}): string {
return `${title} ${options.priority}`;
}
createTicket("Meter display blank", { prio: "P1" });
example.ts(4,39): error TS2353: Object literal may only specify known properties, and 'prio' does not exist in type '{ priority?: string | undefined; }'.
Function types
Functions are values in JavaScript: they can be stored in variables
and passed to other functions as
callbacks. A
function type describes such a
value by its parameters and its result, written like an arrow
function: (kWh: number) => number. The parameter
names are only documentation; the types are what count. Giving the
type a name with type keeps signatures readable:
type PriceRule = (kWh: number) => number;
const flatRate: PriceRule = (kWh) => kWh * 0.32;
const nightRate: PriceRule = (kWh) => kWh * 0.25;
const tiered: PriceRule = (kWh) =>
kWh <= 200 ? kWh * 0.28 : 200 * 0.28 + (kWh - 200) * 0.34;
function bill(kWh: number, rule: PriceRule): string {
return `€${rule(kWh).toFixed(2)}`;
}
for (const rule of [flatRate, nightRate, tiered]) {
console.log(bill(318.4, rule));
}
€101.89 €79.60 €96.26
Notice that (kWh) => … needs no annotation: because
the variable has the type PriceRule, the checker knows
what kWh is. The same happens for callbacks passed to
map, filter and friends, and that is also
how a wrong assumption inside a callback is caught:
const readings = [11.2, 9.8, 12.5];
const labels = readings.map((kWh, day) => `Day ${day + 1}: ${kWh.toFixed(1)} kWh`);
console.log(labels);
const high = readings.filter((kWh) => kWh.startsWith("1"));
example.ts(4,43): error TS2339: Property 'startsWith' does not exist on type 'number'.
A function type can also be written inline as a parameter type. Here
applyToAll accepts any function that turns a number
into a number, whether it is a named function or an arrow written on
the spot:
function applyToAll(readings: number[], convert: (kWh: number) => number): number[] {
return readings.map(convert);
}
const toMWh = (kWh: number): number => kWh / 1000;
console.log(applyToAll([1250, 980, 4100], toMWh));
console.log(applyToAll([11.2, 9.8], (kWh) => Math.round(kWh)));
[ 1.25, 0.98, 4.1 ] [ 11, 10 ]
A function with fewer parameters than the type may still be
used: in JavaScript, ignoring extra arguments is harmless, so
(amount) => … fits
(amount: number, currency: string) => string. A
function that returns the wrong type does not fit:
type PriceRule = (kWh: number) => number;
const peakRate: PriceRule = (kWh) => `${kWh * 0.4}`;
example.ts(2,38): error TS2322: Type 'string' is not assignable to type 'number'.
void: no useful result
void as a return type means the function does not
return a useful value. At run time such a function returns
undefined, as every JavaScript function without
return does. Returning a value from a function declared
void is an error:
function logReading(meterId: string): void {
return meterId.length;
}
example.ts(2,3): error TS2322: Type 'number' is not assignable to type 'void'.
In a function type, however, void is more
relaxed. A callback type that returns void says "the
caller will ignore the result", so a function that happens to return
something is still accepted. That is what allows the short arrow
below, because push returns the new length:
function logReading(meterId: string, kWh: number): void {
console.log(`${meterId}: ${kWh} kWh`);
}
const result = logReading("E-1001", 4.2);
console.log(result);
type Listener = (kWh: number) => void;
const log: number[] = [];
const store: Listener = (kWh) => log.push(kWh); // returns a number: allowed
store(4.2);
store(3.1);
console.log(log);
E-1001: 4.2 kWh undefined [ 4.2, 3.1 ]
The this parameter
In JavaScript, what
this
refers to depends on how a function is called, and a method that is
taken out of its object
loses its this. TypeScript's inferred types do not catch that by themselves:
const meter = {
id: "E-1001",
readings: [11.2, 9.8, 12.5],
total() {
return this.readings.reduce((sum, r) => sum + r, 0);
},
};
const total = meter.total;
console.log(total());
TypeError: Cannot read properties of undefined (reading 'readings')
A this parameter is a fake
first parameter named this that declares what
this must be inside a function. It is not a real
parameter: callers do not pass it, and it is erased from the output.
With it, the checker verifies every call:
interface Meter {
id: string;
readings: number[];
total(this: Meter): number;
}
const meter: Meter = {
id: "E-1001",
readings: [11.2, 9.8, 12.5],
total() {
return this.readings.reduce((sum, r) => sum + r, 0);
},
};
console.log(meter.total().toFixed(1));
const total = meter.total;
console.log(total());
example.ts(17,13): error TS2684: The 'this' context of type 'void' is not assignable to method's 'this' of type 'Meter'.
meter.total() on line 15 is fine; only the detached
call on line 17 is rejected. In classes you rarely need to write
this parameters (see Classes
for arrow-function fields, the usual fix); they matter most for
callbacks in libraries and for event handlers.
Overloads
Some functions accept different kinds of input and return a different type for each. A function overload is one of several signatures declared for the same function, each describing a valid way to call it, followed by a single implementation that handles them all. Callers see only the overload signatures, not the implementation's own:
function formatAmount(amount: number): string;
function formatAmount(amounts: number[]): string[];
function formatAmount(input: number | number[]): string | string[] {
const one = (n: number) => `€${n.toFixed(2)}`;
return Array.isArray(input) ? input.map(one) : one(input);
}
const single = formatAmount(101.888);
const many = formatAmount([32, 25.5]);
console.log(single.toUpperCase(), many.length, many);
€101.89 2 [ '€32.00', '€25.50' ]
The overloads tell the checker that a number gives a
string and an array gives a string[], so
single.toUpperCase() and many.length are
both allowed without further checks. A call that matches no overload
is reported:
function parseReading(value: string): number;
function parseReading(value: number): number;
function parseReading(value: string | number): number {
return typeof value === "string" ? Number.parseFloat(value.replace(",", ".")) : value;
}
console.log(parseReading("4,2"), parseReading(3.1));
console.log(parseReading(true));
example.ts(8,26): error TS2769: No overload matches this call.
The full message adds which overload was tried last and why it
failed. But these overloads are a poor design: both return a
number, and a caller holding a value of type
string | number, say from a form field, cannot call the
function at all, because each overload accepts only one of the two:
function parseReading(value: string): number;
function parseReading(value: number): number;
function parseReading(value: string | number): number {
return typeof value === "string" ? Number.parseFloat(value.replace(",", ".")) : value;
}
function fromForm(input: string | number): number {
return parseReading(input);
}
example.ts(8,23): error TS2769: No overload matches this call.
One signature with a union type says the same thing more simply and works for every caller:
function parseReading(value: string | number): number {
return typeof value === "string" ? Number.parseFloat(value.replace(",", ".")) : value;
}
const inputs: (string | number)[] = [3.1, "4,2", "12.5"];
for (const input of inputs) {
console.log(parseReading(input));
}
3.1 4.2 12.5
Use overloads only when the return type depends on the
input, as with formatAmount; often a
generic function is the better tool even
then.
Async functions
An
async function
always returns a promise, so its return type is written as
Promise<T>, where T is the type of
the value it resolves to. Writing the bare value type is an error,
with a helpful suggestion:
async function loadReading(meterId: string): number {
return 4.2;
}
example.ts(1,46): error TS1064: The return type of an async function or method must be the global Promise<T> type. Did you mean to write 'Promise<number>'?
async function loadReading(meterId: string): Promise<number> {
return meterId === "E-1001" ? 4.2 : 0;
}
loadReading("E-1001").then((kWh) => console.log(kWh.toFixed(2)));
4.20
Typing promises, forgotten awaits and data from APIs
are covered in
Async Code and Typing API Data; functions that work for many types are the subject of
Generics.