Functions
Typed function signatures, optional and rest parameters, function type expressions, and overloads.
TypeScript types every part of a function signature - parameters, return value, and the function itself as a value. Most types are inferred; you only annotate what TypeScript can't figure out on its own.
Parameter types are annotated after the name. Return types are inferred from the return statement, but an explicit annotation is useful for documentation and to catch mistakes early.
function add(a: number, b: number): number {
return a + b;
}
// Arrow function with explicit return type
const multiply = (a: number, b: number): number => a * b;
// Return type inferred as string
const greet = (name: string) => `Hello, ${name}!`;
console.log(add(2, 3)); // 5
console.log(multiply(4, 5)); // 20
console.log(greet("Ada")); // Hello, Ada!Optional parameters use ? after the name. Default parameters use =. TypeScript infers the type from the default value - no annotation needed.
function createUser(
name: string,
role: string = "member",
email?: string,
): string {
const base = `${name} (${role})`;
return email ? `${base} <${email}>` : base;
}
console.log(createUser("Ada")); // "Ada (member)"
console.log(createUser("Grace", "admin")); // "Grace (admin)"
console.log(createUser("Alan", "admin", "a@turing.io")); // "Alan (admin) <a@turing.io>"
// Rest parameters collect remaining args into a typed array
function sum(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3, 4)); // 10A function type expression describes the shape of a function as a value. Use it to type callbacks, higher-order functions, and stored function references.
type Predicate<T> = (value: T) => boolean;
type Transform<A, B> = (input: A) => B;
function filter<T>(arr: T[], pred: Predicate<T>): T[] {
return arr.filter(pred);
}
function map<A, B>(arr: A[], fn: Transform<A, B>): B[] {
return arr.map(fn);
}
const evens = filter([1, 2, 3, 4, 5], (n) => n % 2 === 0);
const doubled = map([1, 2, 3], (n) => n * 2);
console.log(evens); // [2, 4]
console.log(doubled); // [2, 4, 6]Overloads let one function handle multiple distinct call signatures. Write the overload signatures first, then a single implementation signature that unifies them. Callers only see the overloads.
// Overload signatures (visible to callers)
function parse(input: string): number;
function parse(input: number): string;
// Implementation signature (not callable directly)
function parse(input: string | number): number | string {
if (typeof input === "string") return parseInt(input, 10);
return input.toString();
}
const n = parse("42"); // inferred: number
const s = parse(42); // inferred: string
console.log(n, typeof n); // 42 number
console.log(s, typeof s); // "42" stringIn production
Function overloads are the clean way to handle multiple call shapes without leaking any into callers. The implementation signature uses a union of all overload types - it is not callable directly, so callers always get the narrower overload types. Teams that skip overloads often widen the implementation to any to satisfy the type checker, which silently disables type safety for all callers. For callbacks passed across module boundaries, prefer explicit function type expressions over inline annotations - a named type Handler = (event: Event) => void is easier to update and find in search than scattered inline types. When TypeScript infers a return type as void | string, an explicit : string annotation on the function catches the bug at the definition instead of propagating it to every caller.
Enjoyed this? Get more essays on software craft delivered to your inbox.
Subscribe free