Adzbyte
All Articles
DevelopmentTutorials

TypeScript Patterns I Use in Every React Project

Adrian Saycon
Adrian Saycon
January 10, 20262 min read
TypeScript Patterns I Use in Every React Project

TypeScript and React are a natural fit. But TypeScript can feel like it is fighting you if you do not lean into the right patterns. After using TypeScript in React projects for several years, here are the patterns I have found most valuable.

Discriminated Unions for State

Instead of a single state object with optional fields, I use discriminated unions to model different states explicitly:

type FetchState<T> =
    | { status: "idle" }
    | { status: "loading" }
    | { status: "success"; data: T }
    | { status: "error"; error: string };

function useProjects() {
    const [state, setState] = useState<FetchState<Project[]>>({ status: "idle" });

    // TypeScript narrows the type based on status
    if (state.status === "success") {
        // state.data is available and typed as Project[]
    }
}

This eliminates an entire class of bugs where you accidentally access data before it is loaded, or display content when there is an error.

Component Props with Defaults

I always define props as an interface with sensible defaults, rather than making everything required:

interface ButtonProps {
    variant?: "primary" | "secondary" | "ghost";
    size?: "sm" | "md" | "lg";
    children: React.ReactNode;
    onClick?: () => void;
}

function Button({ variant = "primary", size = "md", children, onClick }: ButtonProps) {
    return <button className={`btn-${variant} btn-${size}`} onClick={onClick}>{children}</button>;
}

The key detail: use ? for optional props in the interface, then destructure with defaults in the function signature. This gives you type safety without forcing every consumer to specify every prop.

Const Assertions for Configuration

When defining configuration objects or route maps, as const gives you literal types instead of broad strings:

const ROUTES = {
    home: "/",
    about: "/about",
    services: "/services",
    contact: "/contact",
} as const;

type Route = (typeof ROUTES)[keyof typeof ROUTES];
// Type is "/" | "/about" | "/services" | "/contact"

This catches typos at compile time and gives your IDE perfect autocomplete for route values.

Generic API Fetchers

A typed fetch wrapper eliminates repetitive type casting across your codebase:

async function apiFetch<T>(endpoint: string): Promise<T> {
    const res = await fetch(`${API_BASE}${endpoint}`);
    if (!res.ok) throw new Error(`API error: ${res.status}`);
    return res.json() as Promise<T>;
}

// Usage - fully typed
const projects = await apiFetch<Project[]>("/projects");
const post = await apiFetch<BlogPost>("/posts/my-slug");

Every API call gets type inference automatically. If the shape of your data changes, TypeScript catches every place it is used.

The Payoff

These patterns add minimal overhead but catch real bugs. The discriminated union pattern alone has saved me from countless “cannot read property of undefined” errors. TypeScript is at its best when you model your domain accurately — and these patterns help you do exactly that.

Adrian Saycon

Written by

Adrian Saycon

A developer with a passion for emerging technologies, Adrian Saycon focuses on transforming the latest tech trends into great, functional products.

Discussion (0)

Sign in to join the discussion

No comments yet. Be the first to share your thoughts.