Function Overloads & this Types

Reviewed & published by Brayan K

Function overloads let a single function expose several typed call shapes, while related tools — call signatures, typed optional/default/rest parameters, and the this parameter — give you precise control over how a function is called.

Part of the free TypeScript course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

Overloads are like a menu with set combos. Out back there's one kitchen (the single implementation), but the menu lists distinct, well-defined orders: "Combo A gives you a burger", "Combo B gives you a salad". You order a specific combo and know exactly what you'll get — you never order "the raw kitchen". The this parameter, meanwhile, is the kitchen insisting it must be cooking in its own kitchen, not someone else's.

1. Function Overloads

A function overload is written as several signature lines with no body, followed by one implementation with a body. Each signature describes a distinct, valid way to call the function — and crucially, the return type can differ per signature. The implementation signature must be broad enough to accept every overload, but callers only ever see the specific overloads, never the implementation.

// Overload SIGNATURES (declarations only) + ONE implementation:
function reverse(x: string): string;        // overload 1
function reverse(x: unknown[]): unknown[];  // overload 2
function reverse(x: string | unknown[]): string | unknown[] { // implementation
  if (typeof x === "string") return x.split("").reverse().join("");
  return x.slice().reverse();
}

const a = reverse("abc"); // a: string  -> "cba"
const b = reverse([1, 2]); // b: unknown[] -> [2, 1]
// Callers pick a single overload; the impl signature is hidden from them.
// FUNCTION OVERLOADS let ONE function present SEVERAL typed call shapes.
// You write multiple "overload signatures" (no body) and ONE implementation.
// In TypeScript:
//   function len(x: string): number;          // overload 1
//   function len(x: unknown[]): number;       // overload 2
//   function len(x: string | unknown[]): number { return x.length; } // impl

// At runtime there is only the SINGLE implementation. The overloads are
// compile-time descriptions of the allowed calls. Here is the runtime body:

function len(x) {
  return x.length;   // works for both strings and arrays
}
console.log(len("hello"));      // 5
console.log(len([1, 2, 3]));    // 3

// A more interesting overload: same function, DIFFERENT return shapes.
// In TypeScript:
//   function make(kind: "point"): { x: number; y: number };
//   function make(kind: "name"): string;
function make(kind) {
  if (kind === "point") return { x: 0, y: 0 };
  return "anonymous";
}
console.log(make("point"));  // { x: 0, y: 0 }
console.log(make("name"));   // anonymous

// The implementation signature must be GENERAL enough to cover every overload,
// but callers only see the specific overloads — never the implementation one.

2. Optional, Default & Rest Parameters

Parameters can be optional (title?: string — may be omitted), have a default (greeting = "Hello" — used when no argument is given), or be a rest parameter (...nums: number[] — gathers any number of trailing arguments). The rest parameter must always come last, and a typed rest gives you variadic functions that stay fully checked.

function greet(name: string, greeting = "Hello", title?: string): string {
  const who = title ? title + " " + name : name;
  return greeting + ", " + who + "!";
}

function sum(...nums: number[]): number {
  return nums.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3); // 6  — any count of numbers, all type-checked
// TYPED PARAMETERS come in several flavours:
//   optional:  greet(name: string, title?: string)        // title may be omitted
//   default:   greet(name: string, greeting = "Hello")    // used if not passed
//   rest:      sum(...nums: number[])                      // any count of numbers

// optional + default together:
function greet(name, greeting = "Hello", title) {
  const who = title ? title + " " + name : name;
  return greeting + ", " + who + "!";
}
console.log(greet("Ada"));                      // Hello, Ada!
console.log(greet("Ada", "Hi"));                // Hi, Ada!
console.log(greet("Lovelace", "Hello", "Ms"));  // Hello, Ms Lovelace!

// rest parameter — collects the remaining args into an array:
function sum(...nums) {
  return nums.reduce((a, b) => a + b, 0);
}
console.log(sum());           // 0
console.log(sum(1, 2, 3));    // 6
console.log(sum(10, 20));     // 30

// In a .ts file:
//   function greet(name: string, greeting = "Hello", title?: string): string
//   function sum(...nums: number[]): number

3. Call Signatures & this Parameters

A call signature types an object that is itself callable and carries extra properties — like a counter function that also has a count. A this parameter is a special first parameter that declares what this must be inside the function; it's purely a type annotation (erased at runtime) that stops you calling a method with the wrong receiver.

// Call signature: callable object with a 'count' property
type Counter = { (): number; count: number };

// 'this' parameter: declares the required receiver type (erased at runtime)
type Account = { balance: number };
function deposit(this: Account, amount: number): number {
  this.balance += amount;
  return this.balance;
}
// deposit.call({ balance: 0 }, 50); // ✅  'this' typed as Account
// CALL SIGNATURES describe a callable object's type, optionally with extra
// properties. In TypeScript:
//   type Counter = { (): number; count: number };  // callable AND has 'count'
// And a 'this' PARAMETER types what 'this' refers to inside a function:
//   function reset(this: Counter) { this.count = 0; }
// 'this' is a fake first parameter — it vanishes at runtime, leaving normal JS.

// A callable object with state (the runtime version of a call signature):
function makeCounter() {
  function counter() {
    counter.count += 1;
    return counter.count;
  }
  counter.count = 0;     // extra property on the function object
  return counter;
}

const next = makeCounter();
console.log(next());        // 1
console.log(next());        // 2
console.log(next());        // 3
console.log("count is", next.count); // count is 3

// A method that relies on 'this' — typed in TS as  function(this: T):
const account = {
  balance: 100,
  deposit(amount) {
    this.balance += amount;   // 'this' is the account
    return this.balance;
  },
};
console.log(account.deposit(50)); // 150
console.log(account.deposit(25)); // 175

// In a .ts file:
//   type Counter = { (): number; count: number };
//   deposit(this: { balance: number }, amount: number): number

🎯 Your Turn

Complete the single implementation of an overloaded describe that handles a number or a string. Fill in the two blanks marked ___, then run it.

// 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
// 'describe' is overloaded:  describe(n: number): string  and
//                            describe(s: string): string
// The single implementation handles both. Fill in the blanks.

function describe(value) {
  if (typeof value === "number") {
    return "number: " + value.___(2);   // 👉 .toFixed(2)  -> "number: 3.14"
  }
  return "string of length " + value.___; // 👉 .length  -> "string of length 5"
}

console.log(describe(3.14159));   // number: 3.14
console.log(describe("hello"));   // string of length 5

// ✅ Expected output:
//    number: 3.14
//    string of length 5

Common Errors (and the fix)

Pro Tips

Mini-Challenge: parseInput & average

No blanks this time — just a brief and a starting outline. Build the overloaded parser and the rest-param average yourself, run it, and check your output against the example in the comments.

// 🎯 MINI-CHALLENGE: an overloaded 'parseInput' + a rest param
// In TypeScript:
//   function parseInput(value: string): number;
//   function parseInput(value: number): number;
//   function parseInput(value: string | number): number { ... }
//
// 1. Write parseInput(value): if it's a string, return Number(value);
//    if it's already a number, return it unchanged.
// 2. Write average(...nums) using a REST parameter: parse EACH argument with
//    parseInput, then return the mean (sum / count). Return 0 for no args.
// 3. Log:
//      parseInput("42")            -> 42
//      average("10", 20, "30")     -> 20
//      average()                   -> 0
//
// ✅ Expected output:
//    42
//    20
//    0

// your code here

🎉 Lesson Complete

Practice quiz

How are function overloads written in TypeScript?

  • Several functions with the same body
  • One signature and several bodies
  • Multiple overload signatures (no body) plus ONE implementation with a body
  • Using the 'overload' keyword

Answer: Multiple overload signatures (no body) plus ONE implementation with a body. You declare two or more signature headers with no body, then one implementation that covers them all.

Which signature can callers actually call?

  • Only the overload signatures
  • Only the implementation signature
  • Both equally
  • Neither

Answer: Only the overload signatures. Callers see only the overload signatures; the broad implementation signature is hidden from them.

When do overloads beat a single union-parameter signature?

  • Never
  • When there is only one parameter
  • When performance matters
  • When the RETURN type depends on which argument type was passed

Answer: When the RETURN type depends on which argument type was passed. Overloads express input-to-output relationships, e.g. make("point") returns an object, make("name") a string.

The implementation signature must be:

  • The narrowest of all overloads
  • General enough to cover every overload
  • Identical to the first overload
  • Omitted entirely

Answer: General enough to cover every overload. The implementation params/return must be broad enough (e.g. string | unknown[]) to accept all overloads.

What is a 'this' parameter?

  • A TypeScript-only annotation declaring what 'this' must be, erased at runtime
  • A real first argument callers pass
  • A global variable
  • A class field

Answer: A TypeScript-only annotation declaring what 'this' must be, erased at runtime. A this parameter is a compile-time-only annotation in the first position; it has zero runtime cost.

Where must a rest parameter appear in the parameter list?

  • First
  • Anywhere
  • Last
  • Only with no other parameters

Answer: Last. The rest parameter must always come last.

What does an optional parameter (title?: string) become when omitted?

  • null
  • undefined
  • an empty string
  • a compile error

Answer: undefined. An omitted optional parameter is undefined inside the function.

What does a default parameter (greeting = "Hi") do?

  • Forces the caller to pass it
  • Makes the parameter required
  • Throws if omitted
  • Supplies a value when the argument is missing

Answer: Supplies a value when the argument is missing. A default supplies its value when no argument is given, so it is never undefined.

A call signature like 'type Counter = { (): number; count: number }' describes:

  • A plain object
  • A callable object that also carries extra properties
  • A class
  • A union type

Answer: A callable object that also carries extra properties. A call signature types something that is callable AND has additional members like count.

How should overloads be ordered?

  • Most general to most specific
  • Alphabetically
  • Most specific to most general (TS picks the first match)
  • Order does not matter

Answer: Most specific to most general (TS picks the first match). TypeScript picks the first matching overload, so order from most specific to most general.

Continue this course

Frequently asked questions

How do function overloads work in TypeScript?

You declare two or more overload signatures (just the headers, no body) describing the different valid ways to call the function, followed by ONE implementation with a body whose signature is general enough to cover them all. Callers only ever see and pick the overload signatures — the broad implementation signature is hidden from them.

Why not just use a union type instead of overloads?

A union (x: string | number) is simpler and often enough. Overloads earn their keep when the RETURN type depends on which argument type you passed — e.g. make("point") returns an object but make("name") returns a string. A single union signature can't express that input-to-output relationship; overloads can.

What is a 'this' parameter and why is it not a real argument?

A this parameter (function f(this: SomeType, ...)) is a TypeScript-only annotation in the first position that declares what this should be when the function runs. It has zero runtime cost — it's erased during compilation — but it lets the compiler catch calls where this would be the wrong object.

What's the difference between optional, default, and rest parameters?

Optional (title?: string) may be omitted and is then undefined. Default (greeting = "Hi") supplies a value when the argument is missing. Rest (...nums: number[]) collects any number of trailing arguments into an array. They can be combined, but the rest parameter must always come last.

Related lessons