Generic Callbacks

The Course
•
5 min read

A transform helper has two type relationships: the element the callback receives and the element it returns. mapValues<T, U>(items: readonly T[], transform: (item:T) => U): U[] says each output is the callback’s U, not necessarily the input T.

The caller usually supplies no explicit type arguments because array and callback context provide them. A callback can only assume the fields guaranteed by its input contract. A callback promised to handle every Book cannot safely demand an ExtendedBook containing extra required fields.

Under strictFunctionTypes, ordinary function-typed callbacks are checked against these parameter requirements. There are special compatibility rules for method declarations; prefer a function property or function parameter when expressing a callback contract. We do not need to explore variance terminology to use the safe rule: the callback must handle every input the helper promises to pass.

Keep one generic signature here: T names the input element type and U names the output element type. Their names describe a relationship; neither exists as a runtime value.

Predict, run, and explain

Write the output and trace the important state changes before checking it. Then run the program. Use Check types first; execution is allowed only when the real compiler accepts the source.

Predict, then runTypeScript
function mapValues<T, U>(items: readonly T[], transform: (item: T) => U): U[] {
  return items.map(transform);
}

const lengths = mapValues(["Types", "API"], text => text.length);

console.log(lengths.join(","));

Enable JavaScript to edit and type-check this example.

Check your prediction
5,3

Read the compiler error

The callback requires more than the caller promises to supply. ExtendedBook below explicitly names both required fields; the plain Book passed by apply has no subtitle.

Find and fix the type errorTypeScript
type Book = { title: string };

type ExtendedBook = {
  title: string;

  subtitle: string;
};

function apply(book: Book, render: (book: Book) => string): string {
  return render(book);
}

apply({ title: "Types" }, (book: ExtendedBook) => book.subtitle);

Enable JavaScript to edit and type-check this example.

Check your diagnosis

Accept Book and handle only its guaranteed fields. If subtitle is required by the operation, change the whole input contract instead of narrowing only the callback parameter.

Infer output from what the callback returns

A reusable transformation has separate input and output element types. T describes what the helper supplies; U describes what the callback produces. Keeping both parameters preserves useful information without forcing every transformation to return its input type.

The callback must accept every input allowed by the helper contract. A function that requires an extra property is unsafe even if one current fixture happens to carry it. Check this relationship at the public boundary rather than relying on an accidental richer record.

Infer output from what the callback returns: traceTypeScript
function mapValues<T, U>(
  values: readonly T[],
  transform: (value: T) => U
): U[] {
  return values.map(transform);
}

const lengths = mapValues(["Types", "API"], value => value.length);

console.log(lengths.join(","));
console.log(lengths.every(value => value >= 0));

Enable JavaScript to edit and type-check this example.

Check your prediction
5,3
true

U is number because the callback returns length. A later push of text should fail checking even though the input happened to be text.

Infer output from what the callback returns: contrastTypeScript
type Book = { title: string };

function labels(
  values: readonly Book[],
  render: (book: Book) => string
): string[] {
  return values.map(render);
}

console.log(
  labels([{ title: " Types " }], book => book.title.trim()).join(",")
);
console.log(labels([], book => book.title).length);

Enable JavaScript to edit and type-check this example.

Check your prediction
Types
0

The renderer handles exactly the promised fields. An empty list makes no callback invocations and returns an empty output, which is another part of the transformation contract.

Try the rule on new inputs

Use one input list to produce numbers, booleans and new records in separate calls. Inspect the return types. Then make a callback demand an extra required field and read the parameter-compatibility error.

Your turn

Exercise

Generic Callbacks: practise

Write mapValues and transform books into numeric page counts. Print 10,20. The returned list must be number[], so pushing text should be a compiler error.

Your workspaceTypeScript
// Use separate type parameters for inputs and outputs.

Enable JavaScript to edit and type-check this example.

Hint 1: the idea

The callback determines the result element type.

Hint 2: a first step

Declare transform as (item: T) => U.

Show one solution
One possible solutionTypeScript
function mapValues<T, U>(items: readonly T[], transform: (item: T) => U): U[] {
  return items.map(transform);
}

const pages = mapValues(
  [
    { title: "A", pages: 10 },
    { title: "B", pages: 20 }
  ],
  book => book.pages
);

console.log(pages.join(","));

Enable JavaScript to edit and type-check this example.

10,20

Explain why the implementation meets the stated contract. Which part is enforced by the language or checker, and which part needs behavioral evidence?

Change it and test it

Try a callback requiring an extra required field not present on every input. Explain why this is unsafe even if one demonstration record has that field. Then compare the direct array.map call with the helper: use abstraction only if it adds useful domain behavior.

Was this article helpful?

Your response is saved on this device.