Function Types in TypeScript

Introduction

Functions are where TypeScript’s type system meets everyday logic: you describe what goes in, what comes out, and which calls are legal. This chapter covers parameter and return types, optional and default parameters, rest parameters, arrow function typing, overloads, and the subtle difference between void and undefined as return types.

Prerequisites

Parameter and Return Type Annotations

typescript
function add(a: number, b: number): number {
  return a + b;
}
 
const sum = add(2, 3);

Code explanation:

  • Each parameter gets a type; the return type after ) documents the result
  • Callers must pass two numbers; returning a string would fail type-checking

Named function type (for variables or callbacks):

typescript
type BinaryOp = (left: number, right: number) => number;
 
const multiply: BinaryOp = (left, right) => left * right;

Code explanation:

  • BinaryOp describes any function with that parameter and return shape
  • Implementations assigned to BinaryOp are checked against the alias

Optional Parameters

Use ? for parameters that may be omitted:

typescript
function greet(name: string, title?: string): string {
  if (title) {
    return `Hello, ${title} ${name}`;
  }
  return `Hello, ${name}`;
}
 
greet("Ada");
greet("Ada", "Dr.");

Code explanation:

  • title?: string is equivalent to title: string | undefined for callers
  • Optional parameters must come after required parameters

Default Parameters

Defaults make parameters optional at call sites and give a value when omitted:

typescript
function createUser(name: string, role: string = "member"): { name: string; role: string } {
  return { name, role };
}
 
createUser("Lin");
createUser("Lin", "admin");

Code explanation:

  • role is optional for callers but always a string inside the function
  • TypeScript infers role as string, not string | undefined, when a default exists

Tip

Optional vs Default

Use ? when absence means “no value.” Use = default when absence should mean a specific fallback value.

Rest Parameters

Collect remaining arguments into a typed array:

typescript
function sumAll(...values: number[]): number {
  return values.reduce((total, n) => total + n, 0);
}
 
sumAll(1, 2, 3);

Code explanation:

  • ...values: number[] must be last in the parameter list
  • Callers can pass any number of numbers

Tuple rest (fixed prefix + rest):

typescript
function logEntry(level: string, ...messages: string[]): void {
  messages.forEach((msg) => console.log(`[${level}] ${msg}`));
}

Arrow Functions

Expression form with explicit types:

typescript
const double = (n: number): number => n * 2;
 
const names = ["Ada", "Lin"];
const upper = names.map((name: string): string => name.toUpperCase());

Often parameter types infer from context:

typescript
const lengths = names.map((name) => name.length);

name is inferred as string because names is string[].

Arrow function type alias:

typescript
type Formatter = (value: string) => string;
 
const quote: Formatter = (value) => `"${value}"`;

void vs undefined as Return Types

Return typeMeaningTypical use
voidCaller should ignore the return valueconsole.log wrappers, event handlers
undefinedFunction explicitly returns undefinedRare; APIs that must return undefined
typescript
function logLine(text: string): void {
  console.log(text);
}
 
function noOp(): undefined {
  return undefined;
}

Under strict settings, a function annotated void may return undefined or nothing, but not a meaningful value like number:

typescript
function broken(): void {
  // return 1;  // Error: Type 'number' is not assignable to type 'void'
  return;
}

Code explanation:

  • void means “no useful return” for consumers, not “never returns”
  • Prefer void for side-effect-only functions in application code

Function Overloads

When one function behaves differently by argument shapes, declare multiple call signatures and one implementation:

typescript
function formatInput(value: string): string;
function formatInput(value: number): string;
function formatInput(value: string | number): string {
  if (typeof value === "number") {
    return value.toFixed(2);
  }
  return value.trim();
}
 
const a = formatInput(3.14159);
const b = formatInput("  hello  ");

Code explanation:

  • Overload signatures are what callers see; the implementation signature must be compatible with all of them
  • The implementation body usually uses unions and narrowing (typeof here)

Warning

Keep Overloads Small

Many overloads are hard to maintain. Consider separate functions or a single options object when signatures multiply.

Functions as Values: Call Signatures in Types

For object types that are callable:

typescript
type Logger = {
  (message: string): void;
  level: string;
};
 
const logger: Logger = Object.assign(
  (message: string) => {
    console.log(message);
  },
  { level: "info" }
);

Most day-to-day code uses (args) => Return instead; callable object types appear in libraries.

Practical Example: Order Total

Code explanation:

  • quantity? is optional; ?? 1 supplies a default at runtime
  • Rest parameters accept a variable-length cart

Common Mistakes

IssueSymptomFix
Optional before requiredCompile errorReorder parameters
Wrong return typeAssign result to wrong variableMatch annotation to actual return
Overload implementation too narrowImplementation errorsWiden implementation params with unions
Using any on parametersNo safetyType each parameter

FAQ

Are parameter types required?

With strict / noImplicitAny, yes for functions where types cannot be inferred from context.

Can I use union types on parameters?

Yes. function id(x: string | number) is common; narrow inside the body.

How do I type this in functions?

Use this: SomeType as the first parameter in TypeScript’s fake parameter slot (advanced; classes chapter covers this more).

Is (): void => {} valid?

Prefer (): void => { } or () => undefined for no-op callbacks. Arrow functions returning nothing infer void when used as event handlers.

When should I use overloads vs unions?

Overloads improve autocomplete and documentation when call patterns differ. A single union parameter is simpler when behavior is uniform after narrowing.

Does async change return types?

async functions return Promise<T>. Annotate async function fetch(): Promise<User> (covered more with async JavaScript patterns).