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.
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.
export {};
function greeting(name: string, prefix = "Hi"): string {
return `${prefix}, ${name}`;
}
console.log(greeting("Mira"));
console.log(greeting("Omar", "Welcome"));
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.
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));
[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.