Typorithm · hard

12. Callback variance and readonly boundaries

typescriptstable-typescriptinterviewindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

12. Callback variance and readonly boundaries

0%

Concept and contract

Understand safe substitution and recognize TypeScript’s intentional method exceptions.

A callback consumer may invoke its callback with every value allowed by the parameter type. A callback requiring a narrower subtype is therefore unsafe when the consumer may supply a broader type. This is the practical reason for contravariant parameter checking in ordinary function types under strictFunctionTypes.

Return values move in the other direction: a function returning a richer subtype can satisfy a consumer requiring only the base shape. Explain substitution through an actual call rather than relying solely on covariance and contravariance terminology.

Mechanism and worked example

The example accepts a function that can describe any Animal.

The example accepts a function that can describe any Animal. A handler reading only name is safe for both an Animal and a Dog. A Dog-only handler would require bark even when the consumer supplies a plain Animal, so ordinary function-property checking rejects it.

Method declarations have a deliberate bivariance exception in TypeScript’s compatibility rules. That can admit assignments that are less strict than equivalent function-valued properties. Recognize the exception when reviewing event and collection APIs rather than assuming strictFunctionTypes makes every position fully sound.

Complete worked example

typescript

type Animal = { name: string };
type Dog = Animal & { bark(): void };
function visit(animal: Animal, describe: (value: Animal) => string): string { return describe(animal); }
const description = visit({name:"Milo"}, value => value.name);
Reasoning
A callback accepting every Animal can safely handle the value the consumer supplies.

Counterexamples and common mistakes

The rejected assignment narrows the callback input unsafely.

The rejected assignment narrows the callback input unsafely. Adding a cast would hide a possible missing-method failure. The correct repair is to broaden the callback’s accepted contract, narrow the consumer’s supplied values, or validate before using Dog-specific behavior.

Readonly arrays help avoid granting mutation through a collection reference, but they do not eliminate every aliasing risk or freeze nested elements. A safe API should expose only the capabilities consumers actually need and should not use type compatibility as a substitute for ownership reasoning.

Rejected counterexample: expected compiler diagnostic

typescript

type Animal = { name: string };
type Dog = Animal & { bark(): void };
const dogOnly = (value: Dog): void => { value.bark(); };
// @ts-expect-error A plain Animal may not have bark.
const unsafe: (value: Animal) => void = dogOnly;
Reasoning
A Dog-only callback cannot safely substitute for one that accepts a plain Animal.

Independent practice

Implement visitNames for readonly Animal values and a callback accepting a string name.

Implement visitNames for readonly Animal values and a callback accepting a string name. This deliberately reduces the callback capability to what the operation needs. Test a plain animal and a dog-shaped object without calling dog-only methods.

Then write a separate rejected function-property assignment from a Dog-only handler to an Animal handler. Optional extension: compare method syntax in a tiny compiler fixture and explain the exception; completing the basic task does not require designing explicit variance annotations.

Step 1: Expose only the name string to the callback. Step 2: Visit each input in order.

Practice starter: solve before reading the solution

typescript

type Animal = { name: string };
function visitNames(values: readonly Animal[], callback: (name: string) => void): void { throw new Error("TODO"); }
Reasoning
Reduce the callback contract to the exact name capability the visitor needs.
  • Hint 1: The callback cannot require capabilities the visitor never supplies.
  • Hint 2: A narrower object callback is not automatically substitutable.

Solution, tests and trade-offs

The solution passes only the name string, so callers cannot accidentally depend on extra object capabilities the visitor never promised.

The solution passes only the name string, so callers cannot accidentally depend on extra object capabilities the visitor never promised. This is often a better design than exposing a complex generic callback whose variance is difficult to understand.

Runtime tests confirm every supplied name is visited in order. The negative fixture demonstrates the broader substitution problem. Keeping the concrete API small develops judgment about interface design rather than rewarding a complicated type expression for its own sake.

Practice solution and acceptance checks

typescript

type Animal = { name: string };
function visitNames(values: readonly Animal[], callback: (name: string) => void): void {
  for(const value of values) callback(value.name);
}

// Acceptance checks
const names:string[]=[];visitNames([{name:"A"},{name:"B"}],name=>{names.push(name)});if(names.join(",")!=="A,B")throw new Error("names");
Reasoning
A smaller concrete interface makes the substitution rule obvious and testable.

Interview reasoning and next step

In interviews, show the unsafe call that a narrow callback would receive.

In interviews, show the unsafe call that a narrow callback would receive. Distinguish parameter and return substitution, and mention the method exception when discussing TypeScript’s soundness limits. Prefer a concrete example over a diagram with unexplained arrows.

Continue with overload design. A public signature should make legal calls clear without forcing users to understand every compatibility exception in the implementation.

Optional stretch — after the core task works: Compare method and function-property compatibility in a tiny fixture.

Understand safe substitution and recognize TypeScript’s intentional method exceptions.