Course outline

TypeScript / Basic / Lesson 4

Functions, contracts, and callbacks

Express argument and result relationships, distinguish optional inputs from missing validation, and pass behavior through appropriately typed callbacks.

About 5 min readContent reviewed
On this page

Lesson goals

  • Use parameter, return, optional, default, and rest types.
  • Type callbacks without promising behavior they cannot guarantee.
  • Choose unions or overloads from the actual call contract.

A signature describes permitted calls

A function type relates parameter types to a result type. TypeScript can infer many return types, but an explicit public return type documents a stable boundary and catches accidental changes. Types do not describe every precondition: a number divisor can still be zero and a callback can still throw. Specify units, accepted ranges, side effects, and failure policy alongside the signature. Prefer small functions with observable inputs and outputs rather than hiding changes in unrelated global state. A checker-accepted call means it fits the static contract under current assumptions, not that every runtime operation must succeed.

Defaulting is a runtime operation

An optional parameter may be omitted and is possibly undefined inside the function until handled. A default parameter initializes when the argument is omitted or undefined, while null does not automatically select that default. The first example supplies a prefix default and an explicit return type. Rest parameters collect later arguments into an array whose element type should match the intended calls. Avoid marking a required callback argument optional merely because some callbacks ignore it; optional means the caller of that callback may omit it. Distinguish an omitted argument from an application value that failed validation.

typescript A default and an explicit argument
export {};
function greeting(name: string, prefix = "Hi"): string {
  return `${prefix}, ${name}`;
}
console.log(greeting("Mira"));
console.log(greeting("Omar", "Welcome"));
Expected output
Hi, Mira
Welcome, Omar

The first call uses Hi; the second overrides it. The returned text is ordinary JavaScript string construction.

Pass a capability with a precise input

The second function accepts a callback that transforms one number into another. Its implementation calls the callback once for each value and returns a new array, leaving the input array unchanged. That behavior comes from the implementation, not from the callback type alone. A callback can close over mutable state, perform I/O, or throw even when its signature only mentions numbers. Use readonly input when the function should not mutate through that access path. readonly is a checker restriction on this view and does not freeze every alias of the underlying array.

typescript A typed transformation with visible mutation policy
export {};
function transform(values: readonly number[], operation: (value: number) => number): number[] {
  return values.map(operation);
}
const original = [1, 2, 3];
const result = transform(original, value => value * 2);
console.log(JSON.stringify(result), JSON.stringify(original));
Expected output
[2,4,6] [1,2,3]

The result doubles each input. The implementation creates a new array and preserves the original values.

Do not confuse ignored results with impossible results

A callback type returning void says the consumer ignores its result; TypeScript can permit a value-returning function in that position. It does not promise that the runtime function literally returns undefined. never instead describes a function that does not return normally, such as one that always throws. Overloads can describe distinct input/result relationships, but callers see the overload signatures rather than the implementation signature. Prefer a union parameter when every accepted alternative returns the same kind of result and one implementation handles the union clearly. Extra overloads can make legitimate union-typed calls unnecessarily difficult.

Document invocation and error behavior

State whether a callback runs synchronously, is retained for later use, can be invoked repeatedly, and how its errors propagate. A function taking a callback does not automatically become asynchronous. Likewise, a Promise return type does not prove work is cancelable or parallel. Under strictFunctionTypes, many function-parameter assignments are checked more carefully, but method compatibility has deliberate exceptions discussed later. Test argument omission, defaulting, empty arrays, and callback failure at the API boundary. Do not use an assertion or any to force a callback whose real input requirements are narrower than the values the caller can supply.

Common errors and fixes

An optional callback parameter unexpectedly becomes undefined.

Why it happens
Optional was used to mean the callback may ignore a required value.
How to fix it
Keep the supplied parameter required and let callbacks ignore it.
Prevent it next time
Read optionality from the caller-of-callback perspective.

A void callback actually returns a number.

Why it happens
Ignoring a result was confused with forbidding a runtime value.
How to fix it
Specify the needed result contract explicitly when it matters.
Prevent it next time
Distinguish void, undefined, and never.

Overloads reject a valid union-typed argument.

Why it happens
No visible overload accepts the whole union.
How to fix it
Use a suitable union signature or deliberate overload design.
Prevent it next time
Test callers with inferred and union-typed inputs.

Check your understanding

1. Call greeting("Mira", undefined). Explain the output and why passing null is a different contract under strict checking.

Show worked solution

The output is Hi, Mira because undefined triggers the default initializer. null is not part of the parameter type and is rejected in this checked program; bypassing that rejection would not make null trigger the default at runtime. If null is intentionally supported, model and handle it explicitly instead of suppressing the error.

2. Transform an empty array with the second function, then explain what happens if the callback throws on the second element of a nonempty input.

Show worked solution

An empty input produces a new empty array without callback calls. For a thrown error during map, the function does not return its normal result and the error propagates unless handled by a caller. The original numeric array is unchanged by this implementation, although external side effects performed by the callback before throwing are not rolled back automatically.

Key takeaways

  • Signatures express calls, not every behavioral promise.
  • Make optionality, mutation, and callbacks deliberate.
  • Use overloads only when their relationships help callers.

Official references