---
title: 'Function Types'
source: 'https://academia.sh/en/courses/typescript/function-types'
course: TypeScript
language: en
updated: '2026-08-17T18:09:53+00:00'
license: 'CC BY-SA 4.0'
---

# Function Types

Function type syntax, optional and rest parameters, the void return type's special behavior, the parameter-count rule, overloading, and the this parameter.

The previous lesson used class methods and standalone functions, but how their types
are written was not covered. A function's type is not just parameter and return types:
optional parameters, rest parameters, return types that vary with the call form, and
the `this` context are also part of the type.

This lesson builds these components in order and applies them to a parser that reads
measurement records.

## Function Type Syntax

A function type is written with a parameter list, an arrow, and a return type:

```typescript
type Measurement = { id: string; value: number };
type Filter = (measurement: Measurement) => boolean;

function select(records: readonly Measurement[], filter: Filter): Measurement[] {
  return records.filter(filter);
}

const positive: Filter = (m) => m.value > 0;
const records: Measurement[] = [
  { id: "s-01", value: 21.4 },
  { id: "s-02", value: -3 },
];

console.log(select(records, positive).length);

const wrong: Filter = (m) => m.value;
```

```text
j1.ts(16,30): error TS2322: Type 'number' is not assignable to type 'boolean'.
```

The `positive` declaration writes no type for the `m` parameter; the type comes from
the `Filter` definition through contextual typing. The last line is rejected because
its return type does not match — a number, usable as a truthy value in JavaScript,
cannot be placed where `boolean` is expected.

Parameter names in a function type (`measurement`) are for documentation; they play no
role in compatibility. What matters is order and type.

## Optional, Default, and Rest Parameters

```typescript
type Measurement = { id: string; value: number; note?: string };

function build(
  id: string,
  value: number,
  unit: string = "C",
  ...tags: string[]
): string {
  return `${id}=${value}${unit} [${tags.join(",")}]`;
}

console.log(build("s-01", 21.4));
console.log(build("s-02", 101325, "Pa", "boiler", "shift-3"));

function printNote(measurement: Measurement): string {
  return measurement.note.trim();
}
console.log(printNote({ id: "s-03", value: 1 }));
```

```text
j2.ts(16,10): error TS18048: 'measurement.note' is possibly 'undefined'.
```

The `unit` parameter has a default value; its type is inferred as `string`, and it can
be omitted at the call site. `tags` is a rest parameter; its type is an array, and it
is always defined — if omitted, it becomes an empty array.

The last diagnostic shows the cost of optional fields: `note?: string` makes the
field's type `string | undefined`, and `trim` cannot be called on `undefined`. The
compiler reports the skipped check. The fix is narrowing:
`measurement.note?.trim() ?? ""` or an explicit `if` block.

This behavior depends on the `strictNullChecks` option. With it off, `undefined` is
assignable to every type and this diagnostic never appears — optional fields then
carry no guarantee at all.

## The void Return Type's Special Behavior

`void` means "the return value will not be used." It has a looser rule than expected:

```typescript
type Measurement = { id: string; value: number };
type Recorder = (measurement: Measurement) => void;

const records: Measurement[] = [];
const add: Recorder = (measurement) => records.push(measurement);

add({ id: "s-01", value: 21.4 });
console.log(records.length);

const result = add({ id: "s-02", value: 22.1 });
console.log(result.toFixed(0));
```

```text
j3.ts(11,20): error TS2339: Property 'toFixed' does not exist on type 'void'.
```

Line 5 does **not** error. `records.push(measurement)` returns a number, while
`Recorder` declares a `void` return. The rule: **a function with a return value can be
assigned to a type that returns `void`.** The reasoning: `void` does not say "return
nothing," it says "what you return will not be used."

The consequence shows up on line 11: viewed from the call site, the return type is
`void`, and nothing can be done with it. The value exists in reality; the type system
refuses to see it.

This rule makes functions that take callbacks easier to use. `forEach`'s callback
returns `void`; if every callback had to skip a return value, short forms like
`(m) => records.push(m)` would be rejected.

## The Parameter-Count Rule

In function compatibility, parameter count works in one direction:

```typescript
type Measurement = { id: string; value: number };
type Callback = (measurement: Measurement, index: number) => void;

const recordOnly: Callback = (measurement) => console.log(measurement.id);
recordOnly({ id: "s-01", value: 21.4 }, 0);

const extra: Callback = (measurement, index, extra: string) => console.log(extra);
```

```text
j4.ts(7,7): error TS2322: Type '(measurement: any, index: any, extra: string) => void' is not assignable to type 'Callback'.
  Target signature provides too few arguments. Expected 3 or more, but got 2.
j4.ts(7,26): error TS7006: Parameter 'measurement' implicitly has an 'any' type.
j4.ts(7,39): error TS7006: Parameter 'index' implicitly has an 'any' type.
```

A function taking **fewer** parameters can be assigned to a type that declares more;
it ignores the extra arguments that come in. A function taking **more** parameters
cannot be assigned — the caller will not supply that argument.

This directly reflects JavaScript's call rule: an extra argument causes no error, a
missing one becomes `undefined`. The type system allows the former and blocks the
latter.

Notice the last two diagnostics: on line 7, `measurement` and `index` lost their
contextual types. Because the assignment failed, context could not be established,
and the parameters remained implicit `any`. A single error producing multiple
diagnostics is common; the source diagnostic is the first one.

## Overloading

If a function gives different return types depending on the argument's type, it is
declared with **overloading**:

```typescript
type Measurement = { id: string; value: number };

function parseOne(raw: string): Measurement | null {
  const parts = raw.split("=");
  if (parts.length !== 2) {
    return null;
  }
  const value = Number(parts[1]);
  return Number.isFinite(value) ? { id: parts[0], value } : null;
}

function read(raw: string): Measurement | null;
function read(raw: readonly string[]): Measurement[];
function read(raw: string | readonly string[]): Measurement | Measurement[] | null {
  if (typeof raw === "string") {
    return parseOne(raw);
  }
  return raw.map(parseOne).filter((m): m is Measurement => m !== null);
}

const single = read("s-01=21.4");
const many = read(["s-01=21.4", "broken", "s-02=22.1"]);

console.log(single?.value);
console.log(many.length, many.map((m) => m.id).join(","));
```

Output:

```text
21.4
2 s-01,s-02
```

There are three signatures, but only two can be called. The first two lines are **call
signatures**; the third is the **implementation signature** and is not visible from
outside. `single`'s declared type is `Measurement | null`, `many`'s is
`Measurement[]` — a precise type based on the call form, rather than one single union
type.

That the implementation signature cannot be called shows up with a union-typed
argument. If the three lines below are added to the end of the file above, after a
blank line:

```typescript
declare const ambiguous: string | readonly string[];
const ambiguousResult = read(ambiguous);
console.log(ambiguousResult);
```

```text
j8.ts(28,30): error TS2769: No overload matches this call.
  The last overload gave the following error.
    Argument of type 'string | readonly string[]' is not assignable to parameter of type 'readonly string[]'.
      Type 'string' is not assignable to type 'readonly string[]'.
```

The diagnostic reports only the last overload's mismatch, not every candidate's —
this compiler version condenses the report rather than explaining each signature's
failure. What it never mentions is the implementation signature. Either way, the
caller has to narrow the argument before the call.

Overloading is purely a type-level declaration. A single function remains in the
compiled output, and the implementation body does the branching — that is where the
`typeof raw === "string"` check lives.

## The this Parameter

The context a function is called in can be typed too. When the first parameter's name
is `this`, that parameter is not a real parameter but a context declaration:

```typescript
type Ledger = { name: string; records: number[] };

function summarize(this: Ledger): string {
  return `${this.name}: ${this.records.length}`;
}

const ledger: Ledger & { summarize: () => string } = {
  name: "boiler-2",
  records: [21.4, 22.1],
  summarize,
};

console.log(ledger.summarize());
```

Output is `boiler-2: 2`. If the line `console.log(summarize());` is added to the end
of the file:

```text
this2.ts(14,13): error TS2684: The 'this' context of type 'void' is not assignable to method's 'this' of type 'Ledger'.
```

The `this`-binding rules from the Objects and Functions in JavaScript course are
checked here at the type level: a method called after being detached from its object
loses its context, and the compiler reports it.

The `this` parameter is erased entirely too. In the compiled output of the file above,
the function signature is empty:

```javascript
function summarize() {
    return `${this.name}: ${this.records.length}`;
}
```

## Summary

- A function type is written with a parameter list and a return type; parameter
  names play no role in compatibility.
- Optional fields and parameters add `undefined` to the type; they have to be
  narrowed before use.
- A function with a return value can be assigned to a type that returns `void`, but
  the return value cannot be used at the call site.
- A function taking fewer parameters can be assigned to a type with more
  parameters; the reverse cannot.
- In overloading, call signatures are visible from outside, the implementation
  signature is not; branching happens in the body at runtime.
- The `this` parameter types the context and is erased entirely.

## Next Step

In none of this topic's examples did a value write the name of the type it fit: object
literals were accepted into interfaces, functions into function types, only because
their **shapes** matched. This is TypeScript's fundamental type-compatibility rule,
and it has unexpected consequences. The next lesson takes up structural typing's
rules, excess property checking, and the situations where structural compatibility has
to be overridden.
