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
undefinedto 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
thisparameter 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.