Functions: parameter types, overloads, and this
Beyond typing individual parameters, TypeScript can express optional and default parameters, describe a function that behaves differently depending on its arguments, and even type-check what `this` refers to inside a function — a genuine JavaScript wart TypeScript can catch.
4 min read
Optional and default parameters
function greet(name: string, greeting?: string) {
return `${greeting ?? "Hello"}, ${name}!`;
}
greet("Ada"); // fine — greeting is undefined
greet("Ada", "Hi"); // fine
function greetDefault(name: string, greeting: string = "Hello") {
return `${greeting}, ${name}!`;
}
greetDefault("Ada"); // greeting defaults to "Hello"greeting?: string means the parameter's real type is string | undefined — it can be omitted at the call site, and inside the function body it's genuinely possible for it to be undefined, which is exactly why the first example needs ?? to supply a fallback. greeting: string = "Hello" is different: it's typed as plain string (never undefined) from inside the function's own body, because the default value fills in for a missing argument before the body ever runs — TypeScript treats a defaulted parameter as always having a real value once execution reaches the function body.
Function type expressions: typing a function as a value
type MathOp = (a: number, b: number) => number;
const add: MathOp = (a, b) => a + b;
const multiply: MathOp = (a, b) => a * b;
function calculate(a: number, b: number, op: MathOp): number {
return op(a, b);
}(a: number, b: number) => number describes the shape of a function — its parameter types and return type — as a reusable type, exactly the way an object type describes the shape of an object. This is what lets calculate accept any function matching that shape as its third argument, a direct, function-shaped instance of the structural typing this domain's earlier lesson covers for objects: add and multiply are both accepted because their shapes match MathOp, with no declared relationship to it required.
Overloads: one function, several distinct call signatures
function makeDate(timestamp: number): Date;
function makeDate(month: number, day: number, year: number): Date;
function makeDate(monthOrTimestamp: number, day?: number, year?: number): Date {
if (day !== undefined && year !== undefined) {
return new Date(year, monthOrTimestamp, day);
}
return new Date(monthOrTimestamp);
}
makeDate(12345678); // matches the 1-argument overload
makeDate(1, 15, 2024); // matches the 3-argument overload
makeDate(1, 15); // Error — matches neither declared overloadThe first two lines are overload signatures — they describe the specific, valid ways this function can genuinely be called, and only those combinations are checked against at call sites. The third line, with the actual function body, is the implementation signature — it's never checked against call sites directly (its own parameter types are usually looser, like the optional day?/year? here), it only has to be compatible with every overload above it. This is the right tool specifically when a function's parameter count changes its meaning, not just when parameters are optional — a plain optional-parameter function (like greet above) is simpler and usually the better choice when the shapes don't genuinely diverge like this.
Typing this inside a function — catching a real, classic JavaScript bug
interface Button {
label: string;
onClick(this: Button): void;
}
const button: Button = {
label: "Submit",
onClick() {
console.log(this.label); // fine — this is correctly typed as Button here
},
};
const handler = button.onClick;
handler(); // Error at the call site: 'this' context of type 'void' is not assignable to 'Button'this in JavaScript is notoriously determined by how a function is called, not where it's defined — a method torn off its object (const handler = button.onClick) and called standalone loses its intended this entirely, a genuinely common real bug. Writing this: Button as a function's first "parameter" (it's a special, compile-time-only annotation, never actually passed as a real argument) tells TypeScript what this is supposed to be when the function is called correctly — and critically, TypeScript then flags the exact moment that assumption is violated, right where handler() is called standalone, instead of the bug surfacing later as a confusing runtime undefined error.
Rest parameters: typing "however many more arguments"
function sum(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3); // numbers is [1, 2, 3]
sum(); // numbers is [] — an empty array, not an error...numbers: number[] collects any number of trailing arguments into a genuine array, typed like any other array — the direct TypeScript equivalent of plain JavaScript's own rest-parameter syntax, just with the collected array's element type made explicit and checked.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What's the actual type difference between `greeting?: string` and `greeting: string = "Hello"` as a function parameter?
2. In a set of function overloads, which signature is actually checked against real call sites — the overload signatures, or the implementation signature?
3. Why does declaring `onClick(this: Button): void` on an interface help catch a real bug?