Typorithm · hard

13. Overloads, unions and generic API choices

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

13. Overloads, unions and generic API choices

0%

Concept and contract

Use the simplest signature that preserves the relationship callers actually need.

An overload set exposes several permitted call signatures backed by one implementation. The implementation signature must handle those cases, but is not an additional public overload. This distinction explains why a seemingly broad implementation parameter does not automatically make every broad call legal.

Prefer a union parameter when alternatives share the same result contract. Use a generic when one reusable relationship describes all cases. Reserve overloads for distinct call shapes or input-dependent results that are clearer as explicit public signatures.

Mechanism and worked example

The example maps text to its length and a readonly array to its item count through one union parameter.

The example maps text to its length and a readonly array to its item count through one union parameter. Both alternatives return number, so overloads would add repetition without preserving extra information. The shared length property needs no branch here; branch only when the accepted alternatives require different behavior.

For an overloaded API, a union-valued argument may fail to match either narrow overload even if each individual member would work separately. Add an intentional union overload or choose a generic or union signature if that is a supported public use case.

Complete worked example

typescript

function count(value: string | readonly unknown[]): number { return value.length; }
const total = count(["QA", "PM"]);
Reasoning
A union is sufficient because both accepted input categories produce the same result type.

Counterexamples and common mistakes

The rejected object is outside the declared text-or-array domain.

The rejected object is outside the declared text-or-array domain. Broadening the parameter to any would admit it without supplying meaningful count semantics. A compiler error can reveal that the domain needs clarification rather than another overload.

Do not expose a generic return chosen solely by the caller. A function get<T> returning asserted JSON lets callers request contradictory shapes. Connect output to real inputs or an executable decoder, and test every advertised overload rather than only the implementation body.

Rejected counterexample: expected compiler diagnostic

typescript

function count(value: string | readonly unknown[]): number { return value.length; }
// @ts-expect-error An arbitrary object is outside the domain.
count({ size: 2 });
Reasoning
Unsupported inputs require a domain decision rather than automatic broadening to any.

Independent practice

Implement summarize for a string or a readonly string array, always returning one string.

Implement summarize for a string or a readonly string array, always returning one string. A string should be trimmed; an array should join its items with a comma and space. Begin with the union version and explain why overloads are unnecessary.

Test empty text, empty arrays and two items. Optional extension: design a genuinely input-dependent helper and compare an overload set with a generic mapping. Keep that extension separate so the basic exercise remains a short, readable function.

Step 1: Narrow string versus readonly array. Step 2: Return the documented string result from either branch.

Practice starter: solve before reading the solution

typescript

function summarize(value: string | readonly string[]): string { throw new Error("TODO"); }
Reasoning
Write one union signature before considering whether overloads add consumer value.
  • Hint 1: One result contract does not need duplicate overloads.
  • Hint 2: Empty arrays join to empty text.

Solution, tests and trade-offs

The solution narrows with typeof and returns the same output type from both branches.

The solution narrows with typeof and returns the same output type from both branches. Its public signature states all supported inputs directly. No assertion or overload is required because the input alternatives do not imply different result types.

Compile a rejected numeric argument and run all branch tests. If the API later adds a new input category, define its semantics first. Do not accumulate overloads merely to make an unsupported call compile without a corresponding runtime design.

Practice solution and acceptance checks

typescript

function summarize(value: string | readonly string[]): string {
  return typeof value === "string" ? value.trim() : value.join(", ");
}

// Acceptance checks
if(summarize(" QA ")!=="QA"||summarize([])!==""||summarize(["QA","PM"])!=="QA, PM")throw new Error("summary");
Reasoning
The branches implement exactly the alternatives promised by the public signature.

Interview reasoning and next step

In interviews, compare three signatures for the same small function and justify the simplest one.

In interviews, compare three signatures for the same small function and justify the simplest one. Explain implementation signatures, union-valued callers and overload regression tests. Discuss readability for consumers as well as cleverness inside the library.

Next, branded identifiers show a case where a structural primitive is intentionally distinguished by meaning. That technique should protect a real boundary and remain paired with runtime validation rather than adding ceremony to every string.

Optional stretch — after the core task works: Design an input-dependent result and compare signatures.

Use the simplest signature that preserves the relationship callers actually need.