TypeScript: Everyday Types - discussion (2023-01-13)
The TS handbook's "Everyday Types" section is the baseline, but the part I'm most interested in is how people *actually* apply it in React codebases where route state, intent objects, and stored derived keys are everywhere. Do you keep types shallow and pragmatic, or do you push it until the state contract is basically a schema?
How do you type route/state contracts without turning everything into generics soup? Do you use discriminated unions for intents/actions, or just type callbacks as (value) => void and move on? What types have paid off most for debugging (evidence strings, status enums, revision keys)?
Comments (18)
Back to latestThe biggest payoff for us was typing status lanes as unions.
Once you have pending|ok|error, a ton of bugs become impossible to express.
I prefer shallow types at module boundaries and stricter types for shared contracts (route/doc/vault).
If it's shared state, it's worth being picky.
Discriminated unions for intents are worth it because you can log and snapshot them reliably:
ts
type CartIntent =
| { type: 'add'; sku: string; qty: number }
| { type: 'remove'; sku: string }
| { type: 'setQty'; sku: string; qty: number };
function applyIntent(intent: CartIntent) {
switch (intent.type) {
case 'add':
return;
case 'remove':
return;
case 'setQty':
return;
}
}
Even if the implementation is loose, the intent surface stays strict and debuggable.
Counterpoint: unions can turn into ceremony if you add a new intent every day.
We keep unions for cross-route intents and let local UI handlers be more pragmatic.
That makes sense. For me the dividing line is: will this be recorded/logged/replayed?
If yes, strict. If no, keep it light.
Typing evidence strings sounds silly but it can help.
We have type EvidenceKey = 'freshness' | 'lane' | 'requestId' | 'revision' and it kept debug panels consistent.
Route state typing: we type the object literal and avoid clever generics.
If you need a generic to understand the type, it's not helping the team.
We also use literal types for tags/categories and it prevented a bunch of subtle bugs where a string key drifted.
The forum data itself is a good example: category slugs are a union, not string.
One thing I took from the handbook: you don't need to model everything.
Type the things that are contracts and leave the rest as implementation detail.
Counterpoint: sometimes strict types can hide runtime problems by making you trust the compiler too much.
We still validate external data at runtime and store validation flags as derived keys.
Docs tie-in: the "contract" framing in these sites matches TS well.
Related: API Reference and Core Concepts (Best Practices).
I like typing derived keys because it prevents silent drift (you can't accidentally write totla instead of total).
If you store derived state a lot, spelling becomes correctness.
We started typing "lane" enums everywhere (fetch lane, mutation lane, editor lane).
It made logs and screenshots more consistent because the strings are now standardized.
A misunderstanding I see: people think TS will guarantee state invariants.
It only guarantees what you *say* in the types, which is why deriving validation flags as real keys is still useful.
For route state, we avoid any by keeping defaults concrete.
If the default is typed as a literal, the rest of the app can infer without extra ceremony.
I wish more teams treated "status" strings as types.
If you render status as evidence, you might as well type it too so you can't invent a new status accidentally.
Counterpoint: strictness has a cost in migrations.
We sometimes relax types temporarily, but we keep the public contract strict (what other modules can depend on).
The handbook section is a good reminder that TS types are tools, not goals.
If the type doesn't make the code more readable, it's probably not worth it.
The best ROI types for us were: route keys, intent objects, and status lanes.
Everything else was optional.