On this page
Typing functions
Function type expressions
Every function in TypeScript has a type that describes what arguments it accepts and what it returns. TypeScript calls this a function type expression:
// The type of a function that takes two numbers and returns a number
type BinaryOperation = (a: number, b: number) => number;
// Assigning a function to a variable of this type
const add: BinaryOperation = (a, b) => a + b;
const subtract: BinaryOperation = (a, b) => a - b;
// TypeScript infers the parameter types from BinaryOperation — no need to annotate a and b
const multiply: BinaryOperation = (a, b) => a * b;The arrow syntax (params) => returnType is the standard way to write a function type inline. For more complex signatures, use a call signature inside an interface or type alias.
Parameters
Required parameters
Every parameter without a default or ? marker is required. TypeScript enforces the exact count and types at every call site:
function createUser(name: string, email: string, age: number): object {
return { name, email, age };
}
createUser("Alice", "[email protected]", 30); // OK
createUser("Alice", "[email protected]"); // Error: Expected 3 arguments, but got 2
createUser("Alice", 30, "[email protected]"); // Error: Argument types don't matchOptional parameters
Add ? after the parameter name to make it optional. Optional parameters have the type T | undefined inside the function body:
function buildUrl(base: string, path: string, query?: string): string {
const url = `${base}/${path}`;
return query ? `${url}?${query}` : url;
}
buildUrl("https://api.example.com", "users"); // no query
buildUrl("https://api.example.com", "users", "page=2"); // with queryOptional parameters must appear after all required parameters. You cannot have a required parameter after an optional one.
Default parameters
Default parameters provide a fallback value when the argument is not passed or is explicitly undefined. TypeScript infers the parameter's type from the default value:
function paginate(items: unknown[], page = 1, pageSize = 20): unknown[] {
// page and pageSize are inferred as number
const start = (page - 1) * pageSize;
return items.slice(start, start + pageSize);
}
paginate(data); // page=1, pageSize=20
paginate(data, 2); // page=2, pageSize=20
paginate(data, 3, 10); // page=3, pageSize=10
paginate(data, undefined, 5); // page=1, pageSize=5 — undefined triggers the defaultRest parameters
Rest parameters collect a variable number of trailing arguments into a typed array:
function log(level: "info" | "warn" | "error", ...messages: string[]): void {
console[level](`[${level.toUpperCase()}]`, ...messages);
}
log("info", "Server started");
log("warn", "Retrying", "attempt 2 of 3");
log("error", "Connection failed", "timeout after 5000ms");Callbacks and function types
When a function accepts another function as an argument, you type the callback inline or using a named type:
// Inline callback type
function processItems(
items: string[],
callback: (item: string, index: number) => void
): void {
items.forEach(callback);
}
// Named callback type
type Predicate<T> = (value: T) => boolean;
function filter<T>(items: T[], predicate: Predicate<T>): T[] {
return items.filter(predicate);
}
const numbers = [1, 2, 3, 4, 5, 6];
const evens = filter(numbers, (n) => n % 2 === 0); // [2, 4, 6]void return type for callbacks
When a function receives a callback and ignores its return value, type the callback as returning void. This is more permissive than undefined — a () => void callback can return any value, but callers cannot rely on it.
// The callback returns void — Array.prototype.forEach ignores return values
function forEach<T>(items: T[], callback: (item: T) => void): void {
for (const item of items) {
callback(item);
}
}
// Passing a function that returns a number is valid — return value is ignored
forEach([1, 2, 3], (n) => n * 2); // OK — TypeScript doesn't complain about the return valueCall signatures
When a callable value also has properties, use a call signature inside an interface or type:
interface Logger {
(message: string): void; // call signature
level: "debug" | "info" | "warn" | "error";
timestamp: boolean;
}
const logger: Logger = Object.assign(
(message: string): void => {
const prefix = logger.timestamp ? new Date().toISOString() : "";
console[logger.level](`${prefix} ${message}`);
},
{ level: "info" as const, timestamp: true }
);
logger("Server started"); // callable
logger.level = "debug"; // also has propertiesFunction overloads
Overloads let you declare multiple call signatures for a single function. TypeScript picks the most specific matching signature for each call site:
// Overload signatures — visible to callers
function createElement(tag: "div"): HTMLDivElement;
function createElement(tag: "span"): HTMLSpanElement;
function createElement(tag: "input"): HTMLInputElement;
// Implementation signature — hidden from callers
function createElement(tag: string): HTMLElement {
return document.createElement(tag);
}
const div = createElement("div"); // inferred as HTMLDivElement
const span = createElement("span"); // inferred as HTMLSpanElement
const other = createElement("table"); // Error: Argument '"table"' does not match any overloadOverloads are most useful when a function behaves differently based on argument types and the return type changes accordingly. For cases where only the input type varies but the return type is fixed, a union type is simpler.
The `this` parameter
TypeScript lets you annotate the expected this type as the first parameter (erased at compile time). This prevents calling the function from the wrong context:
interface EventEmitter {
listeners: Map<string, Function[]>;
on(event: string, listener: Function): void;
}
function handleClick(this: EventEmitter, event: MouseEvent): void {
const listeners = this.listeners.get("click") ?? [];
listeners.forEach((fn) => fn(event));
}
// TypeScript enforces that handleClick is called with the correct this context
const emitter: EventEmitter = {
listeners: new Map(),
on(event, listener) {
const list = this.listeners.get(event) ?? [];
this.listeners.set(event, [...list, listener]);
},
};
handleClick.call(emitter, new MouseEvent("click")); // OK
handleClick(new MouseEvent("click")); // Error: 'this' context of type 'void' is not assignableTyping higher-order functions
Higher-order functions (functions that return functions) are common in TypeScript. The return type is itself a function type:
// A function that creates an event handler for a specific action
function createHandler(action: string): (event: Event) => void {
return (event: Event) => {
console.log(`Action: ${action}`, event.target);
};
}
const handleSubmit = createHandler("submit");
const handleCancel = createHandler("cancel");
// Currying — each call returns a function waiting for the next argument
function curry<A, B, C>(fn: (a: A, b: B) => C): (a: A) => (b: B) => C {
return (a) => (b) => fn(a, b);
}
const curriedAdd = curry((a: number, b: number) => a + b);
const addFive = curriedAdd(5);
addFive(3); // 8
addFive(10); // 15Practice
- Write a function
clamp(value: number, min: number, max: number): number. Add JSDoc comments explaining the parameters. Then write a curried versionclampTo(min: number, max: number): (value: number) => number. - Create a
Validator<T>function type(value: T) => boolean. Write three validators for strings:isEmail,isUrl, andhasMinLength(min: number). Compose them into avalidateAllfunction that runs all validators against a value. - Implement a function overload for
serialize(value: string): string,serialize(value: number): string, andserialize(value: boolean): string. Each overload should return a different formatted string.
// Named function with explicit parameter and return types
function add(a: number, b: number): number {
return a + b;
}
// Arrow function — same type information
const multiply = (a: number, b: number): number => a * b;
// Optional parameter — must come after required params
function greet(name: string, title?: string): string {
return title ? `Hello, ${title} ${name}` : `Hello, ${name}`;
}
// Default parameter — implies the type from the default value
function createSlug(text: string, separator = "-"): string {
return text.toLowerCase().replace(/\s+/g, separator);
}
// Rest parameters — typed as an array
function sumAll(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
sumAll(1, 2, 3, 4, 5); // 15
Sign in to track your progress