Functions and Signatures
A function signature is the most valuable annotation you will write — it is a contract the compiler enforces at every call site.
Annotating a function is where types earn the most, because a signature is checked at every place the function is called — not just where it is defined.
function add(a: number, b: number): number {
return a + b;
}
add(2, 3); // 5
add("2", 3); // error at the CALL SITE, where the mistake isParameters and return types#
function greet(name: string): string {
return `Hello, ${name}`;
}
const double = (n: number): number => n * 2;The return type can usually be inferred, and leaving it off is fine for short functions. Annotate it explicitly when:
- the function is exported — it is a contract, and an explicit type stops an accidental change from silently altering it
- the body is long enough that inference is hard for a reader to follow
- you want the compiler to catch a wrong return inside the function rather than at its callers
export function parsePort(raw: string): number {
const n = Number(raw);
return Number.isInteger(n) ? n : 3000;
}Optional and default parameters#
function log(message: string, level?: string) {
console.log(`[${level ?? "info"}] ${message}`);
}
log("started"); // fine — level is undefined
log("failed", "error");level?: string means the type is string | undefined. A default value does the same job and removes the undefined:
function log(message: string, level: string = "info") {
console.log(`[${level}] ${message}`); // level is string, never undefined
}Optional parameters must come after required ones.
Rest parameters#
function sum(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3); // 6
sum(...[4, 5, 6]); // 15void and undefined#
void is the return type of a function that returns nothing useful:
function notify(message: string): void {
console.log(message);
}It is subtly different from undefined: a void return type means "ignore whatever this returns", which is what lets you pass a value-returning function where a void one is expected.
const items: string[] = [];
[1, 2, 3].forEach((n) => items.push(String(n)));
// push returns a number; forEach expects void; this is allowedFunction types#
You can describe the shape of a function, which is how you type callbacks and stored handlers.
type Comparator = (a: number, b: number) => number;
const byValue: Comparator = (a, b) => a - b; // parameters inferred from Comparator
[3, 1, 2].sort(byValue);Note that byValue needed no annotations on a and b — TypeScript infers them from the declared type. This is contextual typing, and it is why callbacks usually need no annotations at all:
["a", "bb"].map((s) => s.length); // s is string, inferred from the arrayTyping a callback parameter#
function fetchUser(
id: string,
onSuccess: (user: User) => void,
onError?: (error: Error) => void,
): void {
// ...
}Writing the callback types out is what makes the caller's arrow function fully typed, with autocomplete on user and no annotations needed at the call site.
Overloads, briefly#
Occasionally one function has genuinely different shapes depending on its arguments:
function parse(input: string): object;
function parse(input: string, asArray: true): unknown[];
function parse(input: string, asArray?: boolean): object | unknown[] {
const value = JSON.parse(input);
return asArray ? [value].flat() : value;
}The first two lines are the signatures callers see; the third is the implementation, which callers cannot call directly. Reach for overloads rarely — a union return type or two separate functions is usually clearer.
Exercise#
// Write a function `formatPrice` that:
// - takes an amount in cents (number) and an optional currency (string, default "USD")
// - returns a string like "$12.34"
// - has explicit parameter and return type annotations
// Then write a `Formatter` function type describing its shape, and assign it.
// write your code hereCommon questions#
Should I always annotate the return type?#
For exported functions, yes — it is a contract, and an explicit annotation means a change to the body cannot silently change what callers receive. For small internal functions, inference is fine and less to maintain.
Why do my callback parameters not need types?#
Contextual typing. When TypeScript already knows the expected function type — from an array method, or from a declared parameter type — it infers the parameter types for you. If your callback parameters are showing as any, the surrounding type is missing or too loose.
What is the difference between void and undefined as a return type?#
undefined means the function must actually return undefined. void means the return value should be ignored, which is more permissive — a function returning a value can be passed where a void-returning one is expected. Use void for callbacks and handlers.
Get the TypeScript agent pack
A battle-tested AGENTS.md, the review checklist, and the failure-mode cheat sheet for TypeScript. One email, then occasional updates when the tooling shifts. No course pitch.
AGENTS.md now — no email needed.