TypeScript: Everyday Types - discussion (2023-05-27)
The Everyday Types chapter looks like basics, but in React apps it turns into a boundary discipline question: do you keep surfaces narrow and owned, or do you let "types" justify wider and wider contracts? I'm curious what people actually do to keep route docs/hook returns small while still being expressive.
When modeling lanes/posture, do you prefer string unions, discriminated objects, or a single structural type? How do you keep boundary parsing wide but internal contracts narrow and stable? Do you type evidence tokens (at least prefixes), or keep them as freeform strings?
Comments (18)
Back to latestString unions for lanes.
Discriminated objects for intent/actions.
We keep boundary types wide using unknown and normalize exactly once.
Then route docs are narrow and boring, and the UI can render evidence for normalized outcomes.
We typed the evidence token prefixes and it reduced chaos:
ts
type Evidence = `lane:${string}` | `scope:${string}` | `query:${string}` | `net:${string}`;
type Contract<T> = { lane: 'idle' | 'pending' | 'ok' | 'error'; value?: T; evidence: Evidence[] };
It's not perfect, but it stops evidence from becoming random prose.
Concrete alternative: don't over-type. Keep types light and rely on runtime evidence instead.
We did that in one codebase and it worked, but only because evidence/log vocabulary was extremely consistent. Without that, it would have become folklore fast.
I think that's the key: types and evidence are two sides of the same contract.
If you keep types light, you need evidence to be strong (and vice versa).
Long-form: the failure mode is "types as permission to widen the contract."
Teams add fields to the route doc because the type says it's ok, but nobody owns the meaning of the fields. The result is a typed junk drawer.
We prevent it by requiring an owner + a one-line tips narrative for every new contract key.
Docs tie-in: the docs' TypeScript guidance pairs well with keeping contracts narrow and observable.
Related: TypeScript and Types and Core Concepts.
Short take: if a type makes debugging harder, it's the wrong type shape.
We treat satisfies as a contract guardrail for normalized objects.
It kept us from over-annotating everything while still preventing accidental widening.
A small thing: model optionality in boundary types, not in internal contracts.
Optional fields inside contracts lead to endless ?. chains and silent fallbacks.
Long-form counterpoint: evidence tokens being typed can also backfire if you turn them into a registry.
We had to keep the token vocabulary small and focused on prefixes; everything else was freeform suffixes.
We used Everyday Types as a team exercise: pick 3 contract shapes and standardize them (lane contract, route doc contract, evidence list).
Once those were stable, everything else became less contentious.
Types are great, but the real win is narrowing your surfaces and making behavior observable.
We also found it helpful to include a contractVersion literal in contract types.
It makes migrations explicit and prevents accidental mixing of versions.
We treat evidence tokens as part of the contract output. If a hook/route doc returns a lane, it must return evidence too.
Otherwise lanes become internal-only and support can't reason about them.
A good check: can you describe the contract in a handful of types without generics gymnastics?
If not, it's probably too big.
Everyday Types is where contract discipline begins, not where it ends.
We use types to encode invariants and we use evidence to encode observations.
If those drift, we treat it as a bug in the contract itself.
Takeaway: don't let TS types become an excuse to widen surfaces.
Keep surfaces narrow, owned, and observable, and TS becomes an accelerator instead of a tax.