AniUI Academy

Functions in TypeScript

Typing parameters and return values, optional and default parameters, rest parameters, overloads, and the type of a function itself.

10 min read

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.");    // fine

A 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); // 10

The 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); // 5

Inside 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 number

Function 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 second

Callers 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

Try it yourself
Loading playground...

What to remember

  • Parameters almost always need an annotation; return types are usually inferred but worth writing explicitly on public functions.
  • param?: T allows omission and adds undefined; param = value provides 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

up to 50
  1. 1.Why must function parameters usually be annotated, unlike most local variables?

  2. 2.In function greet(name: string, greeting = "Hello") {}, what is the inferred type of greeting?

  3. 3.What must be true about optional parameters and rest parameters in a parameter list?

  4. 4.What does explicitly annotating a function's return type actually protect against?

4 left to answer