← TypeScript: types, contracts and diagnosis
03 / 10 · 55 MIN

Functions and callable contracts

Read function signatures as promises about arguments, receiver and completion; identify contracts hiding data or pending work.

The contract comes from both sides of the call

A function type relates parameters to a result. In const format: (value: number) => string, the consumer may supply a number and use the result as text. An arrow assigned in that context can receive its parameter type without repeating an annotation on the arrow itself. This is contextual inference, not absence of checking.

When reviewing a signature, read it in both directions: which arguments must the caller provide, and which result may it use? A default, overload or ignored return changes that relationship. Checking must correspond to actual invocation, including the distinction between synchronous calls and asynchronous work.

Omission and default values

A page?: number parameter can receive undefined because callers may omit it. The body must handle that possibility before using numeric methods. Callers may also pass undefined explicitly. This differs from a required parameter whose type does not admit absence.

An initializer such as count = 3 supplies a value when the argument is undefined or omitted. It does not replace zero merely because zero is falsy. When an application distinguishes zero attempts from missing configuration, its default policy must retain that difference. Exact optional property rules are treated separately; they must not automatically be applied to function parameters.

Callbacks may ignore supplied arguments

If an API always supplies a value and index, both can be required in the callback signature. A consumer using only the value may supply a function with fewer parameters and ignore the index. There is no need to promise the index might be missing merely to permit that consumer.

Writing index?: number says something stronger about the implementation: it permits calling without an index. A callback depending on it must then handle undefined. Correction should start with the API: does it always supply the argument? Then align consumers with that promise. Adding ! in each callback spreads an assumption that belongs explicitly in the shared contract.

void is not a completion protocol

With a contextual () => void type, the caller does not use a useful result. The implementation can return a value ignored by that contract. This explains why a function returning a number can be used in certain callbacks with no observed return. It differs from writing a void return annotation directly on a function body and returning a value incompatible with it.

An async function can likewise leave a promise unobserved when the callback contract ignores the return. The call can return while work continues. If completion matters to the coordinator, represent it, for example with Promise<void>, and actually await that promise in the implementation. Changing only the type without changing flow does not create the wait.

Overloads define public calls

An implementation may need optional parameters or a union to support several public forms. With overloads, the signature used to write the body does not automatically become another signature available to callers. If public signatures admit one or three arguments, a body with two optional parameters does not itself authorize a two-argument call.

Resolution can also fail when each overload accepts a separate member but the consumer has a union variable. If the intended contract accepts either member with the same result, a public union-parameter signature can better express that need. Choose overloads for the relationship they must preserve, not for the number of variants that can be written.

Receiver and positions are also part of the contract

The special this parameter declares the receiver type without adding a runtime positional argument. An API using callback.call(record) supplies record as receiver. An ordinary function can use that this; an arrow retains lexical this from its creation site. The annotation neither binds the receiver nor changes JavaScript call rules.

When spreading an array into a function with two fixed arguments, number[] does not establish exactly two positions. A tuple retains that information. readonly number[] still has variable length, while an as const tuple can preserve two values. The aim is to represent the known shape without turning real uncertainty into an any assertion.

Guided reading of an asynchronous sequence

The example assumes a validated dense string array with no concurrent mutation. The deliver function requires a promise for each call and awaits it before incrementing completed. If the second call rejects, the function does not start the third and its own promise rejects. The counter is not returned on that path. Earlier calls may already have effects: this loop implements neither rollback nor a distributed transaction.

In the cases, first identify who supplies arguments and who needs to observe the result. The index API promises data on each call; the async coordinator needs completion evidence. The correct answer retains those relationships without resolving the warning by losing types or issuing an early notification.

// TypeScript 7.0; strict: true; ES2022 target and library.
// Validated dense input; no concurrent input mutation.
async function deliver(
  messages: readonly string[],
  send: (message: string, index: number) => Promise<void>
): Promise<number> {
  let completed = 0;
  for (const [index, message] of messages.entries()) {
    await send(message, index);
    completed += 1;
  }
  return completed;
}
IN PRACTICE

Trace call order without execution. The consumer may ignore index, but the API always supplies it. Each call fulfills before the next begins. Rejection interrupts flow; earlier completed calls are not rolled back.

Common pitfalls

Making callback parameters optional merely because one consumer ignores them; expecting void to await a promise; treating the body signature as a public overload.

Related topics: Unions, narrowing and state models · Generics and key relationships · External data and error boundaries

Take this idea with you

A useful signature describes what will be supplied and what can be observed. The implementation must retain those promises.

Create account

Reference: TypeScript Handbook: Everyday Types · TypeScript 7.0; explicit strict and host configuration