React Forum
Blog & Guide Responses
Sep 24, 2022 · 04:00 AM

TypeScript: Everyday Types - discussion (2022-09-24)

Started by Lena Fischer
externaltypescripttypescontractsruntime objectsany

I keep seeing teams treat TypeScript as a set of “syntax rules” instead of a way to express contracts across the render tree.

Read the handbook

What are your baseline TS conventions for React code (props/state, event types, unions)? Do you follow the “wide first” posture (accept wide inputs, assert at the boundary), or try to keep everything narrow all the time? Where do you draw the line on any and assertion helpers before it turns into type-shaped vibes?

Comments (10)

Back to latest
Keira Santos
Sep 24, 2022 · 04:06 AM

We got calmer once we treated types as route contracts: boundaries are where you assert, not in random leaf components.

Dmitri Kline
Sep 24, 2022 · 04:14 AM

The “types derived from runtime objects” approach is underrated for UI work.

If the runtime object is the contract, your types stay aligned with what actually ships.

Related: TypeScript and Types and API Reference.

Asha Rahman
Sep 24, 2022 · 04:22 AM

Wide-first boundary assertion is the only thing that scales when inputs are messy:

ts
export const densityDefaults = { mode: 'full', level: 1, label: 'high' };
export type Density = typeof densityDefaults;

export type DensityInput = any;

export function asDensity(input: DensityInput): Density {
  return input as Density;
}

If you keep the assertion at the boundary (route/shell), the rest of the tree gets to be boring.

Noah Vance
Sep 24, 2022 · 04:31 AM

Counterpoint: wide-first + any can become a crutch where nobody feels the contract is real.

If you don’t also render *some* contract marker (version/posture), the tree becomes hard to review.

Lena Fischer
Sep 24, 2022 · 04:36 AM

I’m with you. We ended up pairing boundary assertion with a tiny data-contract marker so QA can tell which shape is active.

Without that, the assertions feel like magic.

Noah Vance
Sep 24, 2022 · 04:39 AM

Exactly. If it’s a contract, make it visible somewhere.

Priya Liao
Sep 24, 2022 · 04:47 AM

We use unions for component surfaces a lot (loading/ok/empty/error).

It’s less about purity and more about making it impossible to “forget an empty state”.

Hana Saito
Sep 24, 2022 · 04:56 AM

Props: strict-ish. External inputs: wide-first.

If you get JSON from anywhere, it’s unknown at the boundary and then asserted into the shape your route expects.

Salma Qureshi
Sep 24, 2022 · 05:07 AM

For event types, we stopped trying to perfectly type every handler and focused on typing the surface we own:

tsx
type FieldSurface = { value: string; onChange: (value: string) => void };

function Field({ value, onChange }: FieldSurface) {
  return <input value={value} onChange={(e) => onChange(e.currentTarget.value)} />;
}

The rest is just plumbing. Type the contract, not the DOM ceremony.

Camille Duarte
Sep 24, 2022 · 05:21 AM

I like satisfies for config-ish objects, because it keeps the runtime object readable.

But most teams don’t use it consistently, so they end up with a mix of inferred and asserted shapes.

Inez Park
Sep 24, 2022 · 05:32 AM

The big failure mode is “types in one file, runtime in another”.

If the runtime object is your contract, keep it close and keep it named.

Benji Rios
Sep 24, 2022 · 05:44 AM

My rule: if you use any, you need a named assertion helper so it’s grep-able.

Raw as any scattered through components is how you end up with “type safety” that only exists in the PR description.