Typing Functions: Parameters, Returns, and Overloads
Learn how to fully type a function's inputs and outputs — including optional, default, and rest parameters, callback and function types, overloads, and the special void, never, and this typings — so function contracts are checked at compile time.
Functions are where most of TypeScript's day-to-day value shows up: once a function's parameters and return type are annotated, every call site is checked automatically, and your editor can tell you immediately when you've passed the wrong type or forgotten an argument.
Parameter and return types
Every parameter can be annotated with name: Type, and the return type goes after the closing parenthesis: function add(a: number, b: number): number { return a + b; }. TypeScript checks that every return statement inside the function actually produces a value of the declared return type, and that every call site supplies arguments of the correct types in the correct order.
You rarely need to annotate the return type explicitly — TypeScript infers it from the function body. Still, many teams require explicit return type annotations on exported/public functions, because it turns an accidental type change in the function body into a clear compile error at the function boundary rather than a mysterious failure somewhere the function is called.
Optional, default, and rest parameters
An optional parameter is marked with ? after its name — function greet(name: string, title?: string) — and must come after all required parameters. Inside the function, an optional parameter's type is a union with undefined (title: string | undefined), so you typically check for its presence before using it.
A default parameter supplies a fallback value directly in the signature — function greet(name: string, title: string = "friend") — and TypeScript infers its type from the default value if you don't annotate one. Unlike optional parameters, a default parameter's type inside the function body does not include undefined, because the default guarantees a value is always present.
Rest parameters collect any number of trailing arguments into a single typed array — function sum(...nums: number[]): number. Only one rest parameter is allowed, and it must be the last parameter in the list. This is how you type functions like Math.max-style variadic APIs.
Function types, callbacks, and generics
A function type describes the shape of a function value on its own, separate from any particular implementation: type MathOp = (a: number, b: number) => number;. This is exactly what you need to type a callback parameter — function apply(a: number, b: number, op: (x: number, y: number) => number): number.
Generic functions (covered in depth in the Generics module) let a function's parameter and return types stay linked without hardcoding a specific type: function first<T>(arr: T[]): T. Combined with callbacks, this produces flexible, type-safe utilities like a typed map or filter.
Function overloads let a single function name support multiple, distinctly different call signatures — for example, a parseInput function that returns a Date when given a string but a number when given a numeric timestamp. You write several signature declarations followed by one general implementation signature that must be compatible with all of them; only the overload signatures are visible to callers.
void, never, this, and async functions
void means a function doesn't return a meaningful value — used for side-effecting functions like event handlers or loggers. It's different from undefined: a void-typed function can technically return undefined, but the caller is not meant to use the result, and callback type positions typed void actually allow returning anything (this lets you pass arr.forEach(x => someArray.push(x)), where push returns a number, into a callback slot typed to return void).
never means a function never successfully returns at all — either it always throws, or it loops forever. function fail(msg: string): never { throw new Error(msg); } is the classic example. never is also central to exhaustiveness checking, covered in the Type Narrowing module.
A function can declare the type of this it expects as a fake first parameter — function handleClick(this: HTMLButtonElement, event: MouseEvent) — which lets TypeScript check that the function is only called in a context where this really is that type, catching a common class of bugs from detached event handlers.
Async functions are typed as returning a Promise of their apparent return value: async function fetchUser(id: number): Promise<User> — even though the function body writes return user;, TypeScript automatically wraps it, and callers must await (or .then()) to get the User out.
Function typing forms
Optional parameters must come after required ones; a parameter can't be both optional and have a default value. Only one rest parameter is allowed, and it must be last. Overload signatures are declarations only — the final signature is the implementation and is not visible to callers.
// Basic
function add(a: number, b: number): number {
return a + b;
}
// Optional & default parameters
function greet(name: string, title?: string, punctuation: string = "!") {
return `Hello, ${title ?? ""} ${name}${punctuation}`;
}
// Rest parameters
function sum(...nums: number[]): number {
return nums.reduce((total, n) => total + n, 0);
}
// Function type / callback
type MathOp = (a: number, b: number) => number;
function apply(a: number, b: number, op: MathOp): number {
return op(a, b);
}
// Overloads
function parseInput(value: string): Date;
function parseInput(value: number): number;
function parseInput(value: string | number): Date | number {
return typeof value === "string" ? new Date(value) : value;
}
// void, never, async
function log(msg: string): void { console.log(msg); }
function fail(msg: string): never { throw new Error(msg); }
async function fetchUser(id: number): Promise<{ id: number; name: string }> {
return { id, name: "Ada" };
}Code Examples
Both parameters and the return type are annotated as number. Calling add with a string argument is caught at compile time, before the code ever runs. Expected result: 5
From the caller's perspective, parseInput looks like two separate, precisely-typed functions — pass a string and get a Date back, pass a number and get a number back — even though there's only one implementation underneath.
function parseInput(value: string): Date;
Common mistake: Skipping the overload signatures and just writing function parseInput(value: string | number): Date | number forces every caller to handle a Date | number union at every call site, even when they know exactly which branch they're in.
function add(a: number, b: number): number {
return a + b;
}
console.log(add(2, 3));
// add("2", 3); // Error: Argument of type 'string' is not assignable to parameter of type 'number'function parseInput(value: string): Date;
function parseInput(value: number): number;
function parseInput(value: string | number): Date | number {
return typeof value === "string" ? new Date(value) : value;
}
const d = parseInput("2026-01-01"); // typed as Date
const n = parseInput(1700000000); // typed as numberOverload resolution
TypeScript checks call sites against the overload list top-to-bottom and picks the first match; only overload signatures are visible outside the function.
Call site Matched signature Result type
----------------------------------------------------------------------
parseInput("2026-01-01") -> parseInput(value: string) -> Date
parseInput(1700000000) -> parseInput(value: number) -> number
(implementation signature is
never matched directly by callers)Important Rules
Annotate parameter types always — TypeScript cannot infer them from usage the way it can infer return types.
Prefer letting TypeScript infer return types for internal helpers, but annotate return types explicitly on exported/public functions to catch accidental signature drift.
Use optional parameters (?) when 'absent' is meaningful; use default parameters when there's a sensible fallback value instead.
Reach for overloads only when call shapes are genuinely different (different argument count or unrelated types) — for related types, a union parameter is usually simpler.
Type callback parameters with a named function type alias when the same shape is reused across multiple functions.
Where this shows up
Typed function signatures are the backbone of any TypeScript API: Express route handlers type their req/res parameters, React event handlers use this-typed or explicitly-typed event objects, and utility libraries like lodash ship elaborate overload sets so that _.map returns the correctly narrowed type whether you pass an array, an object, or a callback with one argument or three.
function greet(name: string, title: string) { /*... */ } greet("Ada"); // Error: expected 2 arguments, got 1 function greet(name: string, title?: string) { /*... */ } greet("Ada"); // OK — title is undefined inside the function
Without ?, TypeScript treats title as required and rejects any call missing it — mark it optional if callers may reasonably omit it.
[multiple-choice] Inside a function, what is the type of an optional parameter title?: string? A. string B. string | undefined C. any D. never
B. string | undefined — Marking a parameter optional with ? makes its type a union with undefined, since the caller may omit it entirely.
[conceptual] What does a return type of never communicate about a function? A. The function returns undefined B. The function returns void C. The function never successfully completes — it always throws or loops forever D. The function's return type could not be inferred
C. The function never successfully completes — it always throws or loops forever — never is reserved for functions that don't return control to the caller at all, such as ones that always throw an error.
[predict-output] Given the overloaded parseInput from the lesson, what is the inferred type of const d = parseInput("2026-01-01")? A. string | number B. Date | number C. Date D. any
C. Date — The call matches the parseInput(value: string): Date overload signature, so TypeScript narrows the result to exactly Date, not the broader union used in the implementation.
Write a function formatPrice(amount: number, currency?: string): string that returns "$19.99" style output, defaulting the currency symbol to "$" when currency is omitted, using either an optional parameter with a manual fallback or a default parameter.
function formatPrice(amount: number, currency: string = "$"): string { return ${currency}${amount.toFixed(2)}; } formatPrice(19.99); // "$19.99" formatPrice(19.99, "€"); // "€19.99"
A default parameter (currency: string = "$") avoids having to check for undefined manually inside the function body.