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 1

Notice 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));
Output
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 exist

Function 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 call

The 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) => boolean describe 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.

Up next · Lesson 4Narrowing, Type Guards and Discriminated UnionsLearn TypeScript narrowing with typeof, instanceof, in, custom type guards, assertion functions and exhaustive discriminated unions using never.