TypeScript Function Types

A function type describes the shape of a function: what parameters it accepts and what it returns, without saying anything about how the function is implemented. Once you can write down a function’s type, you can pass functions around as values — as callback arguments, as object properties, as return values — and have the compiler check that the right “shape” of function is used everywhere. This is one of the most practical parts of TypeScript, because JavaScript treats functions as first-class values constantly (callbacks, event handlers, array methods), and without function types those call sites are a common source of silent bugs.

This lesson covers how to write function type annotations, how TypeScript checks them structurally, how optional parameters, rest parameters, and overloads fit into the picture, and the mistakes that trip people up most often.

Overview: How Function Types Work

Every function in TypeScript has a type, whether you write it explicitly or let the compiler infer it. A function’s type is made up of two parts: the types of its parameters (in order) and the type of its return value. For example, a function that takes two numbers and returns a number has the type (a: number, b: number) => number. That arrow-function-looking syntax is the function type syntax — it is used only in type positions and should not be confused with an arrow function expression, even though it looks similar.

TypeScript checks function types structurally, not nominally. This means two function types are considered compatible if their parameter and return types line up compatibly — there is no need for one to be declared as “implementing” the other. A function value can be assigned to a variable of a given function type as long as:

  • Its parameters are compatible: it’s fine to accept fewer parameters than the target type declares (JavaScript callbacks routinely ignore extra arguments), but each parameter it does declare must be assignable from the corresponding parameter in the target type.
  • Its return type is assignable to the target’s return type (or the target expects void, in which case almost any return type is allowed, since the caller will simply ignore the returned value).

This structural, shape-based checking is what makes passing callbacks to functions like Array.prototype.map or addEventListener feel natural — TypeScript infers the callback’s expected type from context (called contextual typing) so you rarely have to annotate callback parameters yourself.

It’s also important to remember that all of this exists only at compile time. TypeScript’s type checker uses function types to catch mistakes before your code runs, but the emitted JavaScript has no type information at all — parameter and return type annotations are stripped out entirely during compilation. At runtime, a function is just a function; there is no way to ask a compiled JavaScript function what its TypeScript type was.

Syntax

There are three common ways to write a function type:

// 1. Inline function type annotation on a variable
let fn: (a: number, b: number) => number;

// 2. A reusable type alias
type BinaryOp = (a: number, b: number) => number;

// 3. A call signature inside an interface (or object type)
interface BinaryOpInterface {
  (a: number, b: number): number;
}
Part Meaning
(a: number, b: number) The parameter list: names are for documentation/inference only, only the types matter for compatibility.
=> Separates the parameter list from the return type in a function type (not the same as an arrow function’s =>, though it looks identical).
number (after =>) The return type. Use void if the function’s return value should be ignored by callers.
name?: T An optional parameter — must come after all required parameters.
name: T = default A parameter with a default value; it becomes optional for callers.
...name: T[] A rest parameter, collecting any remaining arguments into an array.

Examples

Example 1: Basic function type annotations. A named function already has a type inferred from its signature. You can also declare a variable’s type explicitly and assign a matching function expression to it.

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

const multiply: (a: number, b: number) => number = (a, b) => a * b;

console.log(add(2, 3));
console.log(multiply(2, 3));

Output:

5
6

Notice that multiply‘s parameters a and b don’t need their own type annotations — TypeScript infers them from the variable’s declared function type. This contextual typing is exactly what happens when you pass a callback to a library function.

Example 2: A function type as a parameter (a callback). This is the most common real-world use of function types — describing what shape of callback a higher-order function expects.

type Comparator<T> = (a: T, b: T) => number;

function sortBy<T>(items: T[], compare: Comparator<T>): T[] {
  return [...items].sort(compare);
}

const numbers = [5, 2, 8, 1];
const sorted = sortBy(numbers, (a, b) => a - b);

console.log(sorted);

Output:

[ 1, 2, 5, 8 ]

The generic type alias Comparator<T> describes any function that compares two values of the same type and returns a number (negative, zero, or positive), matching the convention used by Array.prototype.sort. Because sortBy‘s second parameter is typed as Comparator<T>, the compiler infers the types of a and b in the arrow function automatically.

Example 3: Optional parameters, default values, call signatures, and rest parameters.

interface Logger {
  (message: string, level?: "info" | "warn" | "error"): void;
}

const log: Logger = (message, level = "info") => {
  console.log(`[${level.toUpperCase()}] ${message}`);
};

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

log("Server started");
log("Disk almost full", "warn");
console.log(sum(1, 2, 3, 4));

Output:

[INFO] Server started
[WARN] Disk almost full
10

The Logger interface uses a call signature — a way to describe a callable value inside an interface, without a method name. Its level parameter is optional in the type (level?), and the implementation gives it a runtime default of "info", so callers may omit it entirely. The sum function shows a rest parameter, which TypeScript types as an array (number[]) inside the function body.

Under the Hood: How the Compiler Checks Function Types

When the compiler checks whether a function value is assignable to a target function type, it performs a structural comparison, roughly following these steps:

  • Parameter count: the source function may declare the same number of parameters as the target, or fewer. A source function may never require more parameters than the target type provides, since callers following the target type won’t supply extras.
  • Parameter types: for each parameter the source function does declare, its type must accept the corresponding parameter type from the target (parameters are checked contravariantly in strict mode for function-typed variables, meaning the source parameter type must be the same as or a supertype of the target’s parameter type).
  • Return type: the source function’s return type must be assignable to the target’s return type. A source returning a more specific type than needed is fine (e.g. returning "info" where string is expected); a target expecting void accepts any return type, since the value is simply discarded by callers.

Overloaded functions add another layer: you can declare multiple call signatures for the same function name, and the compiler picks the first matching overload when checking a call site.

function toArray(x: string): string[];
function toArray(x: number): number[];
function toArray(x: string | number): string[] | number[] {
  if (typeof x === "string") {
    return x.split("");
  }
  return [x];
}

const chars = toArray("hi");
const nums = toArray(42);

console.log(chars);
console.log(nums);

Output:

[ 'h', 'i' ]
[ 42 ]

Only the overload signatures (the two lines without a body) are visible to callers; the final signature with the body is the implementation signature and must be general enough to cover every overload, but it is not itself a callable overload from the outside.

Finally, remember that none of this survives compilation. Every annotation shown above — : number, => void, the Comparator<T> alias, the overload signatures — is erased. The JavaScript emitted for add, for instance, is just function add(a, b) { return a + b; }. Type checking is a compile-time-only safety net; it has zero effect on how the function behaves at runtime.

Common Mistakes

Mistake 1: Assigning a function whose parameter type doesn’t match the target type.

type Handler = (event: string) => void;

const handler: Handler = (event: number) => {
  console.log(event);
};

This fails to compile with an error like Type '(event: number) => void' is not assignable to type 'Handler'. Types of parameters 'event' and 'event' are incompatible. Type 'string' is not assignable to type 'number'. Explicitly annotating the parameter as number conflicts with the Handler type, which requires a string parameter. The fix is to either remove the annotation (letting it be inferred as string from context) or match it explicitly:

type Handler = (event: string) => void;

const handler: Handler = (event) => {
  console.log(event.toUpperCase());
};

handler("click");

Output:

CLICK

Mistake 2: Returning the wrong type from a typed function value.

type Provider = () => string;

const getName: Provider = () => {
  return 42;
};

The compiler reports Type 'number' is not assignable to type 'string'. because the Provider type promises callers a string, but the implementation returns a number. The fix is to return a value of the promised type:

type Provider = () => string;

const getName: Provider = () => {
  return "Ada";
};

console.log(getName());

Output:

Ada

Best Practices

  • Prefer a named type alias (or interface call signature) for any function type you reuse in more than one place — it documents intent and gives better error messages than repeating the inline signature.
  • Let contextual typing do the work for callback parameters (e.g. inside .map(), .filter(), or a typed callback parameter) instead of re-annotating parameter types that the compiler already knows.
  • Avoid the bare Function type — it accepts any callable value with any arguments and any return type, which defeats the purpose of typing your functions. Always write out the specific signature you expect.
  • Use void as a return type when callers should not rely on the return value, rather than undefined, so implementations remain free to return something without breaking the type.
  • Reach for function overloads only when a single union-typed signature can’t express the relationship between input and output types; otherwise a plain union parameter is simpler to read and maintain.
  • Keep the implementation signature of an overloaded function private to that function — it should not be part of the public API surface callers rely on.

Practice Exercises

  • Write a type alias Predicate<T> for a function that takes a value of type T and returns a boolean. Then write a generic filterArray function that takes an array and a Predicate<T> and returns the filtered array.
  • Declare an interface with a call signature representing a function that takes a string and an optional numeric times parameter (defaulting to 1) and returns a string repeated that many times. Implement it and call it both with and without the second argument.
  • Write two overload signatures for a function parseValue: one that takes a string and returns a number (parsed), and one that takes a number and returns that same number unchanged. Implement it with a single implementation signature using a union parameter type.

Summary

  • A function type describes a function’s parameter types (in order) and its return type, written as (param: T, ...) => ReturnType.
  • TypeScript checks function types structurally: a function is assignable to a function type if it accepts compatible (or fewer) parameters and returns a compatible type.
  • Function types can be written inline, as a type alias, or as a call signature inside an interface.
  • Optional parameters (?), default parameters, and rest parameters (...) all have well-defined function-type equivalents.
  • Overloads let one function name have multiple distinct call signatures, resolved by the first match at each call site.
  • All function type information is erased at compile time — it exists purely to catch mistakes before the code runs.