Lesson 4 of 25
Functions in TypeScript
Typing parameters and return values, optional and default parameters, rest parameters, overloads, and the type of a function itself.
Every function is a contract: given these inputs, here's what comes back. TypeScript lets you write that contract down and has the compiler enforce it on every call.
Typing parameters and return values
function add(a: number, b: number): number {
return a + b;
}Parameters almost always need an annotation — TypeScript infers a
variable's type from its initial value, but a parameter has no value until
someone calls the function, so there's nothing to infer from. Leave it off
and, under the recommended noImplicitAny setting, that's a compile error
rather than a silent any.
The return type (: number after the parentheses) is usually optional —
TypeScript infers it from what the function body actually returns:
function add(a: number, b: number) {
return a + b; // return type inferred as number
}Writing it explicitly anyway is good practice on anything other people call, because it turns a change to the function's internals into an error at the function itself, rather than a wave of confusing errors at every call site if the inferred return type quietly shifts.
Arrow functions
Exactly the same rules apply — the type annotations just move around:
const add = (a: number, b: number): number => a + b;
const isEven = (n: number): boolean => n % 2 === 0;Optional and default parameters
A parameter followed by ? may be omitted by the caller — its type
automatically includes undefined:
function greet(name: string, title?: string) {
return title ? `Hello, ${title} ${name}` : `Hello, ${name}`;
}
greet("Diaz"); // fine
greet("Diaz", "Dr."); // fineA default value achieves something similar, but the parameter is not
undefined inside the function body — it's whatever the default resolved
to:
function greet(name: string, greeting = "Hello") {
return `${greeting}, ${name}!`; // greeting is always a string here
}
greet("Diaz"); // "Hello, Diaz!"
greet("Diaz", "Welcome"); // "Welcome, Diaz!"Optional parameters must come after required ones — function f(a?: string, b: string) is a compile error, since the compiler can't tell whether a
single argument at a call site is meant for a or b.
Rest parameters
Collects any number of trailing arguments into a real array, and must be the last parameter:
function sum(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3, 4); // 10The type of a function itself
A function is a value, and it has its own type — useful for parameters that accept a callback, or a variable that will be assigned a function later:
type MathOp = (a: number, b: number) => number;
function applyOp(a: number, b: number, op: MathOp): number {
return op(a, b);
}
applyOp(2, 3, (a, b) => a + b); // 5Inside applyOp, the parameters a and b in the arrow function don't need
their own annotations — TypeScript already knows op must be a MathOp, so
it infers each parameter's type from that contextual type. This is called
contextual typing, and it's why callbacks passed to array methods like
.map() don't usually need annotations either:
const doubled = [1, 2, 3].map((n) => n * 2); // n inferred as numberFunction overloads
Occasionally a function's return type genuinely depends on which shape of arguments was passed, not just their types — something a single signature can't express precisely. Overloads let you declare several call signatures before the one real implementation:
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 - 1, day);
}
return new Date(monthOrTimestamp);
}
makeDate(1_700_000_000_000); // matches the first overload
makeDate(3, 15, 2024); // matches the secondCallers only ever see the overload signatures, not the combined
implementation signature — makeDate(3, 15) (missing year) is a compile
error, even though the implementation signature itself technically allows
day to be undefined. Reach for overloads sparingly; a union parameter
type is usually simpler and expresses the same idea for most cases.
Try it yourself
What to remember
- Parameters almost always need an annotation; return types are usually inferred but worth writing explicitly on public functions.
param?: Tallows omission and addsundefined;param = valueprovides a default and infers the type from it.- Optional parameters must follow required ones; a rest parameter must be last.
- A function passed where a specific function type is expected gets its parameter types inferred for free — contextual typing.
Check yourself
4 questions · pass 3/4 to unlock Object Types and Interfaces
1.Why must function parameters usually be annotated, unlike most local variables?
2.In
function greet(name: string, greeting = "Hello") {}, what is the inferred type ofgreeting?3.What must be true about optional parameters and rest parameters in a parameter list?
4.What does explicitly annotating a function's return type actually protect against?
4 left to answer