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.