Surf‑Side Pair Review in Santa Teresa
Sunrise painted the Pacific pink while Andrés and I huddled over a hotspot at a Costa Rican surf hostel. His pull request introduced a new React modal, but the props
object was typed as any
. One mis‑spelled field crashed staging—minutes before a demo in Bogotá. We refactored on the spot: explicit interfaces, union guards, and a generic hook that eliminated three runtime checks. The demo shipped, and we still caught the morning waves. Today’s post distills the patterns that saved us that dawn.
Why Type Safety Matters in 2025
Component libraries grow faster than onboarding docs, and remote teams juggle time zones where a production bug might sleep eight hours before rescue. TypeScript bolts a static safety net onto React—catching prop mismatches, state shape drift, and brittle ref forwarding before code reaches CI. With server components and Suspense now mainstream, implicit any
values sneak in from data‑fetching layers. Mastering type‑safe patterns keeps junior developers productive and senior reviewers sane—whether you’re coding on Dominican fiber or Colombian café Wi‑Fi.
Toolbelt at a Glance
Tool / Concept | One‑liner purpose |
---|---|
TypeScript 5.4 | Superset of JS with static typing and inference. |
@types/react | Community definitions that teach TypeScript React’s API. |
ts‑node | Run TS files directly; great for snippet experiments. |
eslint‑plugin‑react‑hooks | Lints dependency arrays to avoid stale props. |
CLI Command | What it does |
---|---|
npm i -D typescript | Adds the TypeScript compiler. |
npx tsc --init | Generates a baseline tsconfig.json . |
npm i -D @types/react @types/react-dom | Installs type defs for plain JS packages. |
npm i -D eslint @typescript-eslint/eslint-plugin | Lints TS + React code. |
Concept Primer — Plain English
- Structural Typing: Objects are compatible by shape, not class—ideal for flexible props.
- Generics: Type “variables” that make hooks reusable without sacrificing safety.
- Discriminated Unions: Combine variants with a shared tag, enabling exhaustive
switch
checks. - Utility Types: Helpers like
Partial<T>
orComponentProps<'button'>
reduce duplication.
Step‑by‑Step Patterns
1. Typed Component Props
tsxCopyEdit// Avatar.tsx
type AvatarProps = {
src: string;
alt: string;
size?: number; // defaults work with ?
};
export function Avatar({ src, alt, size = 48 }: AvatarProps) {
return <img src={src} alt={alt} width={size} height={size} />;
}
Explanation—AvatarProps
surfaces required (src
, alt
) and optional (size
) properties. IntelliSense guides juniors; CI blocks missing alt‑text.
2. Generic Hooks for Reusable State
tsxCopyEdit// useLocalStorage.ts
import { useState, useLayoutEffect } from 'react';
export function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
const cached = localStorage.getItem(key);
return cached ? (JSON.parse(cached) as T) : initial;
});
useLayoutEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
Line‑by‑line
<T>
defers the type until the hook is called (useLocalStorage<number>('score', 0)
), eliminating casts.as const
preserves tuple inference, so consumers get[value: number, setValue: Dispatch<number>]
automatically.
3. Discriminated Unions for API Responses
tsCopyEdittype Loading = { status: 'loading' };
type Error = { status: 'error'; message: string };
type Success<T> = { status: 'success'; data: T };
export type FetchState<T> = Loading | Error | Success<T>;
tsxCopyEditfunction UserCard({ userId }: { userId: string }) {
const state = useUser(userId); // returns FetchState<User>
switch (state.status) {
case 'loading':
return <>…</>;
case 'error':
return <p role="alert">{state.message}</p>;
case 'success':
return <p>{state.data.name}</p>;
}
}
Why it rocks—Add a new variant ('stale'
) and TypeScript forces every switch
to handle it, preventing uncaught null pointers in production.
4. Safer Polymorphic Components
tsxCopyEdit// Button.tsx
import { ElementType, ComponentProps } from 'react';
type Props<E extends ElementType> = {
as?: E;
variant?: 'primary' | 'ghost';
} & ComponentProps<E>;
export function Button<E extends ElementType = 'button'>({
as,
variant = 'primary',
...rest
}: Props<E>) {
const Tag: ElementType = as || 'button';
return <Tag className={`btn-${variant}`} {...rest} />;
}
Now <Button as="a" href="/about" />
gets href
autocompletion, while <Button onClick={…}>
stays strictly typed.
Common Pitfalls & Fixes
- React.FC Overuse
React.FC
forces implicitchildren
andReactNode
return, hiding generic props. Fix: prefer plain function declarations with explicit return types. any
Escape Hatches
Temporary casts become permanent tech debt. Fix: pair‑review// @ts-expect-error
annotations; setnoImplicitAny
totrue
.- Stale Dependencies in Hooks
Forgetting to list a dispatch callback inuseEffect
can create ghost state. ESLint with the hooks plugin catches this at commit time.
Remote‑Work Insight Box
During code reviews from Panama to Porto, TypeScript’s red squiggles bridge time gaps. A teammate in Brazil surfaces compile errors at 2 a.m. my time, so I wake to actionable comments instead of vague bug reports—accelerating asynchronous collaboration.
Performance & Accessibility Checkpoints
- Compile Time: Large projects may hit 30‑second transpiles on low‑tier laptops. Enable
incremental: true
andskipLibCheck
intsconfig.json
. - Bundle Size: Types vanish at runtime, but Rome/SWC bundlers can exclude
.d.ts
faster than Babel. Measure withsource-map-explorer
. - Strict A11y: Augment TS with
@radix-ui/react-aria-live
types to ensure live regions compile. - Lighthouse: Verify typed components still pass color‑contrast audits; types won’t fix missing
aria‑label
attributes—your linter should. - DevTools: React DevTools now displays source maps keyed to TS lines; set
inlineSources
for jump‑to‑definition on Chrome.
Wrapping Up — A Pattern Checklist
- Define prop interfaces; no
any
, noReact.FC
crutches. - Use generics to build hooks and polymorphic components that adapt without casts.
- Embrace discriminated unions for resilient async states.
- Lean on ESLint + TypeScript rules to flag stale dependencies and unsafe casts.
- Optimize ts‑config for compile speed—critical when your hotspot throttles in rural Mexico.
Type safety isn’t about pleasing a compiler; it’s about freeing mental RAM for design decisions and keeping remote teams aligned across oceans. Try a small refactor today—your future self will thank you from the next time zone.