Function Types & Signatures

intermediate functions types signatures

In simple language, a function type tells us what a function takes in and what it gives back. TypeScript lets us describe that shape so we can pass functions around safely.

The simplest way is to annotate the parameters and return type directly on the function declaration. The compiler then yells at us if we call it wrong.

function add(a: number, b: number): number {
  return a + b;
}

// inferred return type — TS figures it out
function double(x: number) {
  return x * 2; // inferred as number
}

Function type expressions

When we want to pass a function as a parameter (think: callbacks), we describe its shape with an arrow-style type. This is the most common pattern we’ll see.

type Greeter = (name: string) => string;

const hello: Greeter = (name) => `Hi, ${name}`;

function run(fn: (n: number) => boolean) {
  return fn(42);
}

The only difference between this and a regular type alias is the => — that’s how we say “this is a function that returns X”.

Call signatures (the object form)

Sometimes a function also has properties on it (like express’s app() which is callable but also has app.get). For those, we use call signatures inside an object type.

type CountedFn = {
  calls: number;
  (x: number): number; // call signature — no `=>`
};

function makeCounter(): CountedFn {
  const fn = ((x: number) => x * 2) as CountedFn;
  fn.calls = 0;
  return fn;
}

Construct signatures

For things meant to be called with new, we use new (...). Rare in app code, common in library typings.

type CtorOf<T> = new (...args: any[]) => T;

function create<T>(C: CtorOf<T>): T {
  return new C();
}

void vs undefined return

A void return means “the caller shouldn’t rely on the return value”. It does NOT mean the function must return nothing — it just means the value is ignored. This catches people off guard in interviews.

type Logger = (msg: string) => void;

const log: Logger = (msg) => msg.length; // OK — return is ignored

Why this matters

In real code, function signatures are the contract between modules. Get them right and refactors stay safe; get them wrong and runtime errors leak through. A common interview question: “What’s the difference between a function type expression and a call signature?” — expressions are for plain callable types, call signatures let us add properties.