Function Overloads and Typing this

Intermediate
11 min

Function Overloads and Typing this

JavaScript functions often behave differently depending on the arguments they receive: parse("1") returns a number, parse(["1", "2"]) returns an array. A single union-typed signature loses that relationship and forces callers to narrow the result. Overloads describe each valid call shape precisely. This lesson also covers the this parameter, which lets you type what this refers to inside a function, and the polymorphic this return type behind fluent APIs.

Declaring Overloads

An overloaded function has one or more overload signatures (declarations without a body) followed by exactly one implementation signature with the body:

typescript
function createElement(tag: "a"): HTMLAnchorElement; function createElement(tag: "canvas"): HTMLCanvasElement; function createElement(tag: string): HTMLElement; function createElement(tag: string): HTMLElement { return document.createElement(tag); } const link = createElement("a"); // HTMLAnchorElement const box = createElement("section"); // HTMLElement

Rules that trip people up:

  • Callers only see the overload signatures. The implementation signature is invisible externally, even though it must be compatible with every overload.
  • TypeScript tries overloads top to bottom and picks the first match, so put the most specific signatures first. If tag: string came first, "a" would resolve to HTMLElement.
  • The implementation body is checked only against the implementation signature, so it usually uses unions and runtime checks (typeof, Array.isArray) to branch.

Overloads work on methods and on interfaces too:

typescript
interface Formatter { format(value: number): string; format(value: Date, locale?: string): string; }

When Not to Overload

Overloads add maintenance cost. Reach for simpler tools when they suffice:

| Situation | Better choice | |---|---| | Parameter is optional or has a default | function f(a: string, b?: number) | | Same behaviour for several input types | Union parameter: id: string \| number | | Return type depends on input type generically | Generics: function first<T>(xs: T[]): T | | Return type depends on a literal argument | Overloads, or a generic with conditional types |

Use overloads when the return type changes with the argument shape and a generic would be harder to read.

The this Parameter

In JavaScript, this is decided at call time. TypeScript lets you declare what this must be by adding a fake first parameter named this. It is erased from the compiled output and does not count as a real argument:

typescript
interface Button { label: string; el: HTMLElement } function handleClick(this: Button, event: MouseEvent) { console.log(`${this.label} clicked at ${event.clientX}`); } const btn: Button = { label: "Save", el: document.createElement("button") }; btn.el.addEventListener("click", handleClick.bind(btn)); handleClick(new MouseEvent("click")); // Error: The 'this' context of type 'void' is not assignable to method's 'this' of type 'Button'

With noImplicitThis (enabled by strict), using this in a plain function without such an annotation is an error, which catches the classic bug of losing this when a method is passed as a callback. Arrow functions never have their own this; they capture it from the enclosing scope and need no annotation. this: void declares that a function must not rely on this at all.

Polymorphic this in Classes

Inside a class, this used as a type means "the type of the current instance", including subclasses. Returning this from methods enables chaining that keeps the subclass type:

typescript
class QueryBuilder { protected clauses: string[] = []; where(cond: string): this { this.clauses.push(`WHERE ${cond}`); return this; } limit(n: number): this { this.clauses.push(`LIMIT ${n}`); return this; } } class UserQuery extends QueryBuilder { active(): this { return this.where("active = 1"); } } new UserQuery().where("age > 18").active().limit(10); // .active() is still available after .where() because it returns `this`, not QueryBuilder

Had where returned QueryBuilder, the call to .active() would fail after it.

Common mistakes

  • Placing a broad overload before a specific one, so the specific signature is never selected.
  • Writing an implementation signature that is narrower than an overload; TypeScript reports "This overload signature is not compatible with its implementation signature".
  • Passing a method as a callback without bind or an arrow wrapper, then wondering why this is undefined.
Quick Quiz
Question 1 of 3

Which signature can callers of an overloaded function see?

Key Takeaways

  • Overloads are a list of signatures without bodies plus one implementation signature that covers them all.
  • Order overloads from most specific to most general; TypeScript picks the first match.
  • Prefer optional parameters, unions or generics when they express the API more simply.
  • A this parameter types the receiver and is erased at compile time; noImplicitThis catches untyped this.
  • Returning the this type from methods keeps subclass types intact in fluent chains.

Next lesson: Classes in TypeScript — constructors, access modifiers, inheritance and class members with types.

Function Overloads and Typing this - TypeScript | CodeYourCraft | CodeYourCraft