Skip to main content

Functions

Functions are the workhorses of any program, and TypeScript's type system makes them dramatically safer. Typed parameters prevent you from passing the wrong argument; return type annotations catch inconsistencies in what you return; and function overloads let you precisely describe APIs that behave differently depending on the input.

Typed parameters and return types​

The most basic improvement TypeScript adds to functions is parameter and return type annotations:

function add(a: number, b: number): number {
return a + b;
}

function greet(name: string): string {
return `Hello, ${name}!`;
}

function logError(message: string, code: number): void {
console.error(`[${code}] ${message}`);
}

The return type annotation (: number, : string, : void) comes after the closing parenthesis. TypeScript also infers return types from the function body - the annotation is optional but recommended for public functions:

// TypeScript infers the return type as number
function multiply(x: number, y: number) {
return x * y;
}

// Explicit annotation makes the contract clear and catches mistakes:
function divide(a: number, b: number): number {
if (b === 0) {
// return "error"; // Error: Type 'string' is not assignable to type 'number'
throw new Error("Division by zero");
}
return a / b;
}

Optional parameters​

Parameters marked with ? are optional - they can be omitted by the caller, and their type inside the function is T | undefined:

function createSlug(text: string, separator?: string): string {
const sep = separator ?? "-";
return text
.toLowerCase()
.replace(/\s+/g, sep)
.replace(/[^a-z0-9-_]/g, "");
}

createSlug("Hello World"); // "hello-world"
createSlug("Hello World", "_"); // "hello_world"

Optional parameters must come after required parameters:

// OK
function search(query: string, limit?: number, offset?: number): void { /* ... */ }

// Error: A required parameter cannot follow an optional parameter
// function broken(limit?: number, query: string): void { ... }

Default parameters​

Default parameters provide a value when the argument is undefined or omitted. They are cleaner than optional parameters when a sensible default exists:

function paginate(
items: unknown[],
page: number = 1,
pageSize: number = 20
): { data: unknown[]; page: number; total: number } {
const start = (page - 1) * pageSize;
return {
data: items.slice(start, start + pageSize),
page,
total: items.length,
};
}

const items = ['a', 'b', 'c'];

paginate(items); // page=1, pageSize=20
paginate(items, 2); // page=2, pageSize=20
paginate(items, 1, 50); // page=1, pageSize=50
paginate(items, undefined, 50); // page=1 (default), pageSize=50

Default parameters give the function a concrete type - no | undefined inside the body:

function formatDate(date: Date, locale: string = "en-US"): string {
// locale is string here (not string | undefined), because it has a default
return date.toLocaleDateString(locale);
}

Rest parameters​

Rest parameters collect a variable number of arguments into an array:

function sum(...numbers: number[]): number {
return numbers.reduce((acc, n) => acc + n, 0);
}

sum(1, 2, 3); // 6
sum(10, 20, 30, 40); // 100
sum(); // 0

// Rest parameters can follow regular parameters
function logWithPrefix(prefix: string, ...messages: string[]): void {
messages.forEach(msg => console.log(`[${prefix}] ${msg}`));
}

logWithPrefix("INFO", "Server started", "Listening on port 3000");

Rest parameters must be last in the parameter list. Combining with typed tuples (TypeScript 4.0+):

type Middleware = [
path: string,
handler: (req: { path: string }, res: { send: (body: string) => void }) => void,
];

function applyMiddleware(app: unknown, ...middleware: Middleware[]): void {
middleware.forEach(([path, handler]) => {
console.log(`Registering handler for ${path}`);
});
}

Function types​

Functions are first-class values in TypeScript. You can describe their type and use them as parameters or return values:

// Function type annotation
type Predicate<T> = (item: T) => boolean;
type Transformer<T, U> = (input: T) => U;
type Comparator<T> = (a: T, b: T) => number;

// Using function types as parameters
function filter<T>(items: T[], predicate: Predicate<T>): T[] {
return items.filter(predicate);
}

function sortBy<T>(items: T[], compare: Comparator<T>): T[] {
return [...items].sort(compare);
}

// Concrete usage
const users = [
{ name: "Charlie", age: 25 },
{ name: "Alice", age: 30 },
{ name: "Bob", age: 20 },
];

const adults = filter(users, u => u.age >= 21);
const sorted = sortBy(users, (a, b) => a.name.localeCompare(b.name));

Inline function type syntax​

// In interface
interface Button {
label: string;
onClick: (event: MouseEvent) => void;
onHover?: (event: MouseEvent) => void;
}

// In function parameter
function debounce(fn: (...args: unknown[]) => void, ms: number): (...args: unknown[]) => void {
let timer: ReturnType<typeof setTimeout>;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), ms);
};
}

Arrow functions​

Arrow functions in TypeScript are typed the same way as regular functions:

// Explicit types
const multiply = (a: number, b: number): number => a * b;

// Type inferred from context
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2); // TypeScript knows n is number
const evens = numbers.filter(n => n % 2 === 0);

// Arrow function stored in a typed variable
type MathOp = (a: number, b: number) => number;

const add: MathOp = (a, b) => a + b;
const subtract: MathOp = (a, b) => a - b;

Arrow functions and this​

Arrow functions do not have their own this - they capture it from the enclosing scope. This is often what you want for callbacks:

class Timer {
private count = 0;

start(): void {
// Arrow function: this refers to the Timer instance
setInterval(() => {
this.count++;
console.log(`Tick: ${this.count}`);
}, 1000);
}
}

With a regular function in setInterval, this would be undefined (in strict mode) or the global object - a classic JavaScript bug.

void vs never​

These two return types both indicate "no meaningful return value" but mean different things:

Return typeMeaningExample
voidFunction completes normally but returns no valueconsole.log, event handlers
neverFunction never completes (throws or loops forever)Error throwers, infinite loops
function logRequest(url: string): void {
console.log(`GET ${url}`);
// returns normally (implicitly returns undefined)
}

function fail(message: string): never {
throw new Error(message);
// TypeScript knows this never reaches the end
}

function assertDefined<T>(value: T | null | undefined, name: string): T {
if (value == null) {
fail(`${name} is required`); // TypeScript knows execution stops here
}
return value; // TypeScript knows value is T (not null/undefined)
}

Function overloads​

Overloads let you declare multiple signatures for a single function - useful when the return type depends on the input:

// Overload signatures (no implementation body)
function formatValue(value: string): string;
function formatValue(value: number): string;
function formatValue(value: boolean): string;
function formatValue(value: Date): string;

// Implementation signature (handles all cases)
function formatValue(value: string | number | boolean | Date): string {
if (typeof value === "string") return value;
if (typeof value === "number") return value.toLocaleString();
if (typeof value === "boolean") return value ? "Yes" : "No";
return value.toLocaleDateString();
}

formatValue("hello"); // "hello"
formatValue(1234567); // "1,234,567"
formatValue(true); // "Yes"
formatValue(new Date()); // e.g. "4/18/2026" (locale-dependent)

A more practical example - a function that returns different types based on input:

function createElement(tag: "div"): HTMLDivElement;
function createElement(tag: "input"): HTMLInputElement;
function createElement(tag: "canvas"): HTMLCanvasElement;
function createElement(tag: string): HTMLElement;
function createElement(tag: string): HTMLElement {
return document.createElement(tag);
}

const div = createElement("div"); // HTMLDivElement - has .innerHTML, etc.
const input = createElement("input"); // HTMLInputElement - has .value, .checked, etc.
const canvas = createElement("canvas"); // HTMLCanvasElement - has .getContext(), etc.

Overloads with optional behaviour​

// parseJSON returns T when a validator is provided, or unknown without one
function parseJSON(text: string): unknown;
function parseJSON<T>(text: string, validate: (v: unknown) => v is T): T;
function parseJSON<T>(text: string, validate?: (v: unknown) => v is T): unknown | T {
const parsed = JSON.parse(text);
if (validate) {
if (!validate(parsed)) throw new Error("Validation failed");
return parsed as T;
}
return parsed;
}

Generic functions​

Functions can be generic, accepting type parameters that are inferred from the arguments:

function identity<T>(value: T): T {
return value;
}

identity("hello"); // T is string, returns string
identity(42); // T is number, returns number
identity([1, 2, 3]); // T is number[], returns number[]

function first<T>(array: T[]): T | undefined {
return array[0];
}

function zip<A, B>(a: A[], b: B[]): [A, B][] {
const length = Math.min(a.length, b.length);
return Array.from({ length }, (_, i) => [a[i], b[i]]);
}

zip([1, 2, 3], ["a", "b", "c"]); // [1, "a"], [2, "b"], [3, "c"]

We cover generics in depth in chapter 6.

The this parameter​

TypeScript lets you declare an explicit this parameter to prevent accidental misuse:

interface Greeter {
name: string;
greet(this: Greeter): string;
}

const greeter: Greeter = {
name: "World",
greet() {
return `Hello, ${this.name}`;
},
};

greeter.greet(); // OK
const fn = greeter.greet;
// fn(); // Error: The 'this' context of type 'void' is not assignable to type 'Greeter'

The this parameter is erased in the compiled output - it is purely a type-checking aid.

Practical example: an HTTP client wrapper​

Here is a realistic function design combining several concepts:

type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";

interface RequestOptions {
headers?: Record<string, string>;
timeout?: number;
signal?: AbortSignal;
}

interface ApiResponse<T> {
data: T;
status: number;
headers: Record<string, string>;
}

type ApiError = {
message: string;
status: number;
code?: string;
};

async function request<T>(
method: HttpMethod,
url: string,
body?: unknown,
options: RequestOptions = {}
): Promise<ApiResponse<T>> {
const { headers = {}, timeout = 10_000 } = options;

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);

try {
const response = await fetch(url, {
method,
headers: {
"Content-Type": "application/json",
...headers,
},
body: body !== undefined ? JSON.stringify(body) : undefined,
signal: options.signal ?? controller.signal,
});

clearTimeout(timeoutId);

const data: T = await response.json();

return {
data,
status: response.status,
headers: Object.fromEntries(response.headers.entries()),
};
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}

// Convenience wrappers
const get = <T>(url: string, options?: RequestOptions) =>
request<T>("GET", url, undefined, options);

const post = <T>(url: string, body: unknown, options?: RequestOptions) =>
request<T>("POST", url, body, options);

// Usage
interface User {
id: number;
name: string;
email: string;
}

const { data: user } = await get<User>("/api/users/1");
console.log(user.name); // TypeScript knows user is User

Summary​

  • Parameter types prevent wrong arguments; return type annotations catch inconsistent returns
  • Optional parameters (?) and default parameters (=) cover different use cases
  • Rest parameters (...args: T[]) collect variable-length arguments into an array
  • Function types ((a: T) => U) let you pass functions as parameters or return them
  • Arrow functions are typed identically to regular functions; they capture this lexically
  • void means "returns nothing normally"; never means "never returns"
  • Function overloads let you describe functions whose return type depends on input
  • Generic functions infer type parameters from their arguments

Next up: Classes - class syntax, access modifiers, constructors, inheritance, abstract classes, and implementing interfaces.