Skip to content
academia.sh

Lesson 10 / 23

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.

Contents

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:

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;
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

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 }));
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:

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));
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:

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);
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:

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:

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:

declare const ambiguous: string | readonly string[];
const ambiguousResult = read(ambiguous);
console.log(ambiguousResult);
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:

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:

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:

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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close