Knowing When to Stop

Expert · 20 min read · ▶ live playground · ✦ checkpoint

Knowing when to stop is what turns type-level programming from a party trick into engineering. The best type is the smallest promise your team can read, test, and maintain.

Type golf is solving a type problem with the cleverest or shortest type possible. Engineering is choosing the readable design that keeps real code honest. You have seen the power: branches, recursion, string parsing, tuple work. Now you need the brake pedal.

Prefer data-derived promises

Start with the boring source of truth. If your app already has a small runtime list, derive the type from that list. Do not write a route parser or recursive string machine when a plain tuple, an array type with fixed positions, is clearer.

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

→ Mina subscribed to Launch created

This is not impressive. That is the point. The value list is visible. The type follows it. The label map must cover every event and cannot invent extra keys. A future reader can change the event list and let the checker show the affected sites.

Spend a compile-time budget

A compile-time budget is the time your checker and editor spend understanding your types. Editor latency is the delay before hover, autocomplete, or diagnostics respond while you work.

You do not need scary recursive examples to feel this. Huge generated unions, nested conditional types, and broad public re-export files already taught in Section 17 can make the checker do more work. Measure before and after you introduce a clever helper:

# read-only measurement loop
npx tsc --noEmit --extendedDiagnostics
# edit the type helper
npx tsc --noEmit --extendedDiagnostics

The number is not the whole story. Also watch your editor: does hover lag on the files that use the helper, does autocomplete pause inside common components, do diagnostics arrive late after normal edits? If yes, reduce the shape. Name intermediate aliases. Shrink generated unions. Move work to runtime code when runtime code is clearer.

Use the escape valve honestly

An escape valve is a small, documented move that accepts a local checker limitation instead of building a worse abstraction. An as assertion is a type claim written with as; it is still a costume, so use it only when nearby code has already checked the fact and the checker cannot see it.

Here the runtime helper checks a string against the event list. A 40-line conditional type would not make the program safer. A tiny local assertion, tied to the check above it, is easier to review.

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

→ accepted karma.awarded

This is not permission to spray assertions over errors. The receipt is the runtime check. Without that check, as EventName is just a costume on an unknown string. With the check beside it, the assertion is a narrow bridge between runtime proof and checker knowledge.

Compute only at boundaries

A boundary is a place where a contract crosses from one part of the program to another: route definitions, API events, database tables, package exports, and public helper signatures. Compute types there when it removes duplicated promises.

Inside ordinary business logic, prefer named shapes and normal functions. If a launch screen needs a label, write a function. If a public API needs "payload for this event name," a small type helper earns its place.

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

→ Mina launched launch_search on pro

That helper computes at the API boundary. It links one event name to one payload shape. The rest of the code stays plain. This is the rule of thumb: derive types from data first; compute types only where a boundary would otherwise duplicate a promise.

Use these questions before adding a clever type:

  1. Can I derive this from a runtime list or object instead?
  2. Can a named alias make the current type readable?
  3. Does this protect a boundary, or only impress the author?
  4. Can I measure the checker/editor cost before and after?
  5. Would a small runtime helper plus a documented local assertion be more honest?

Part V ends here. You have used TypeScript in the browser, React, servers, APIs, databases, CLIs, libraries, and the type layer; the same rule held everywhere: make promises clearer than the code they replace.

Checkpoint

Answer all three to mark this lesson complete

+50 XP on completion