Foundations Updated 2026-09 View as Markdown

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.

typescript
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 is

Parameters and return types#

typescript
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
typescript
export function parsePort(raw: string): number {
  const n = Number(raw);
  return Number.isInteger(n) ? n : 3000;
}

Optional and default parameters#

typescript
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:

typescript
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#

typescript
function sum(...numbers: number[]): number {
  return numbers.reduce((total, n) => total + n, 0);
}

sum(1, 2, 3);          // 6
sum(...[4, 5, 6]);     // 15

void and undefined#

void is the return type of a function that returns nothing useful:

typescript
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.

typescript
const items: string[] = [];
[1, 2, 3].forEach((n) => items.push(String(n)));
// push returns a number; forEach expects void; this is allowed

Function types#

You can describe the shape of a function, which is how you type callbacks and stored handlers.

typescript
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:

typescript
["a", "bb"].map((s) => s.length);      // s is string, inferred from the array

Typing a callback parameter#

typescript
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:

typescript
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#

typescript
// 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 here

Common 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.

Unsubscribe in one click. We never sell the list. Or just take the AGENTS.md now — no email needed.