TypeScript · Lesson 3 of 12
Typing Functions and Overloads
Type TypeScript functions properly: parameters, return types, optional and default params, rest args, callbacks, function types and overloads.
- Beginner
- 15 min read
- 4 objectives
Before this lessonLesson 2: Types, Interfaces and Unions
What you will learn
- Annotate parameters and return types
- Use optional, default and rest parameters
- Describe callbacks with function types
- Write overloads only when a union is not enough
Your Progress
0 of 12 lessons 0%
- Lessons0 / 12
- Completed0
- Est. time left~ 3 hours
Create a free account to keep your progress on every device.
Tip: pressing Next marks this lesson complete automatically.
Functions are where most bugs enter a codebase: someone passes a string where a number was expected, forgets an argument, or ignores that a function can return undefined. Typing a function writes its contract down, so the compiler checks every call site for you.
This lesson covers the everyday tools (parameter and return types, optional and rest parameters, callbacks) and then the one advanced tool you will occasionally need: overloads.
Parameters and return types
Parameters always need a type, because TypeScript cannot guess what callers will pass. Return types are usually inferred, but writing them on exported functions is a good habit: it documents intent and stops an accidental change in the body from silently changing the public API.
function add(a: number, b: number): number {
return a + b;
}
// Arrow functions use the same syntax
const formatPrice = (cents: number): string => `$${(cents / 100).toFixed(2)}`;
// void means "the caller should not use the return value"
function logOrder(id: string): void {
console.log(`order ${id}`);
}
add(2, 3); // OK
// add("2", 3); // Error: Argument of type 'string' is not assignable to parameter of type 'number'
// add(2); // Error: Expected 2 arguments, but got 1Notice that TypeScript also checks the number of arguments. In plain JavaScript, add(2) would quietly return NaN.
Optional, default and rest parameters
A ? makes a parameter optional, which means its type inside the function includes undefined. A default value is often nicer, because the parameter is then never undefined in the body and TypeScript infers its type from the default.
function greet(name: string, title?: string): string {
// title is string | undefined here
return title ? `Hello, ${title} ${name}` : `Hello, ${name}`;
}
function paginate(page = 1, pageSize = 20): string {
// page and pageSize are inferred as number
return `offset=${(page - 1) * pageSize}&limit=${pageSize}`;
}
// Rest parameters collect any number of arguments into a typed array
function sum(...nums: number[]): number {
return nums.reduce((total, n) => total + n, 0);
}Here is the same code as plain JavaScript so you can run it and see what the types are protecting:
function greet(name, title) {
return title ? `Hello, ${title} ${name}` : `Hello, ${name}`;
}
function paginate(page = 1, pageSize = 20) {
return `offset=${(page - 1) * pageSize}&limit=${pageSize}`;
}
function sum(...nums) {
return nums.reduce((total, n) => total + n, 0);
}
console.log(greet("Ada"));
console.log(greet("Ada", "Dr."));
console.log(paginate());
console.log(paginate(3));
console.log(sum(4, 5, 6));Hello, Ada Hello, Dr. Ada offset=0&limit=20 offset=40&limit=20 15
Options objects instead of long parameter lists
Once a function takes more than three or so parameters, calls like createUser("Ada", true, false, 3) become unreadable. Pass one object instead and destructure it. The type describes every option by name, and call sites become self-documenting.
interface CreateUserOptions {
name: string;
email: string;
isAdmin?: boolean;
plan?: "free" | "pro";
}
function createUser({ name, email, isAdmin = false, plan = "free" }: CreateUserOptions) {
return { id: crypto.randomUUID(), name, email, isAdmin, plan };
}
createUser({ name: "Ada", email: "ada@stackcone.com", plan: "pro" });
// createUser({ name: "Ada", email: "ada@stackcone.com", plna: "pro" });
// Error: Object literal may only specify known properties, and 'plna' does not existFunction types and callbacks
Functions are values, so they have types too. A function type looks like an arrow: (input: Params) => Result. You use these whenever a function accepts or returns another function, which in JavaScript means callbacks, event handlers and middleware.
type Predicate<T> = (item: T) => boolean;
function filterBy<T>(items: T[], keep: Predicate<T>): T[] {
const out: T[] = [];
for (const item of items) if (keep(item)) out.push(item);
return out;
}
interface Order { id: string; total: number; }
const orders: Order[] = [
{ id: "A1", total: 120 },
{ id: "A2", total: 40 },
];
// The callback's parameter is inferred as Order: no annotation needed
const big = filterBy(orders, (o) => o.total > 100);That last line shows contextual typing: because filterBy declares what the callback receives, TypeScript types o for you. This is why you rarely annotate callback parameters passed to map, filter or addEventListener.
A function type with a void return is also flexible: a callback typed () => void may return something, and TypeScript simply ignores it. That is what lets you write items.forEach((x) => list.push(x)) even though push returns a number.
Overloads
Sometimes a function's return type depends on what you pass in. A union return type loses that link: callers always get the full union and must narrow it. Overloads let you list several call signatures, followed by one implementation that handles all of them.
// Overload signatures (what callers see)
function parseInput(value: string): number;
function parseInput(value: string[]): number[];
// Implementation signature (hidden from callers, must be compatible with all overloads)
function parseInput(value: string | string[]): number | number[] {
return Array.isArray(value) ? value.map(Number) : Number(value);
}
const one = parseInput("42"); // number
const many = parseInput(["1", "2"]); // number[]
// parseInput(42); // Error: No overload matches this callThe implementation signature is not callable from outside; only the overloads above it are. TypeScript checks overloads top to bottom and picks the first one that matches, so put the most specific signatures first.
Recap
- Always type parameters; annotate return types on exported functions.
- Prefer default values to
?when a sensible default exists; use rest parameters for variable argument lists. - Switch to a single options object once a function has several parameters.
- Function types like
(item: T) => booleandescribe callbacks, and contextual typing fills in callback parameters. - Use overloads only when the return type truly depends on the argument type and a union or generic cannot express it.
// Write your solution here
Finished reading? Mark this lesson complete to track your progress.
