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.
An overloaded function has one or more overload signatures (declarations without a body) followed by exactly one implementation signature with the body:
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"); // HTMLElementRules that trip people up:
tag: string came first, "a" would resolve to HTMLElement.typeof, Array.isArray) to branch.Overloads work on methods and on interfaces too:
interface Formatter {
format(value: number): string;
format(value: Date, locale?: string): string;
}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.
this ParameterIn 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:
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.
this in ClassesInside 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:
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 QueryBuilderHad where returned QueryBuilder, the call to .active() would fail after it.
bind or an arrow wrapper, then wondering why this is undefined.Which signature can callers of an overloaded function see?
this parameter types the receiver and is erased at compile time; noImplicitThis catches untyped this.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.