Hien Phan
Hien Phan
Strategy
BlogProjectsAbout
StrategyBlogProjectsAbout

Apps

🎯English📹YouTube🚀Indie⚡Learn⚙️Dashboard
Back to Writing

TypeScript Best Practices for React Developers

Essential TypeScript patterns and practices for building type-safe React applications.

January 15, 2026•5 min read•Boilerplate Team
TypeScript
TypeScript
React
Best Practices
TypeScript Best Practices for React Developers

TypeScript transforms how we write React applications. Here are the essential patterns you should know.

Why TypeScript?

TypeScript adds static typing to JavaScript, catching errors at compile time, improving IDE support, and making refactoring safer. It's become the de facto standard for React development.

Component Props Typing

Interface vs Type

For component props, use interfaces rather than type aliases. Interfaces are extendable and better for object shapes:

interface ButtonProps {
  children: React.ReactNode;
  onClick?: () => void;
  variant?: "primary" | "secondary";
  disabled?: boolean;
}

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

Use React.ReactNode for children props as it accepts any renderable value (strings, numbers, elements, arrays, etc.).

Generic Components

Create reusable components with generics. This is powerful for list components and data tables:

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyFn: (item: T) => string;
  emptyMessage?: string;
}

export function List<T>({
  items,
  renderItem,
  keyFn,
  emptyMessage = "No items found"
}: ListProps<T>) {
  if (items.length === 0) {
    return <div className="text-muted-foreground">{emptyMessage}</div>;
  }

  return (
    <ul className="space-y-2">
      {items.map((item) => (
        <li key={keyFn(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// Usage with users
<List
  items={users}
  renderItem={(user) => <div>{user.name}</div>}
  keyFn={(user) => user.id}
/>

// Usage with posts
<List
  items={posts}
  renderItem={(post) => <div>{post.title}</div>}
  keyFn={(post) => post.slug}
/>

Hook Typing

Type your custom hooks properly. Return an object for named exports:

interface UseAsyncResult<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
  refetch: () => Promise<void>;
}

export function useAsync<T>(
  asyncFunction: () => Promise<T>,
  deps: any[] = []
): UseAsyncResult<T> {
  const [state, setState] = useState<UseAsyncResult<T>>({
    data: null,
    loading: true,
    error: null,
    refetch: async () => {}
  });

  const fetch = async () => {
    setState(prev => ({ ...prev, loading: true, error: null }));
    try {
      const data = await asyncFunction();
      setState({ data, loading: false, error: null, refetch: fetch });
    } catch (error) {
      setState({
        data: null,
        loading: false,
        error: error instanceof Error ? error.message : "Unknown error",
        refetch: fetch
      });
    }
  };

  useEffect(() => {
    fetch();
  }, deps);

  return state;
}

Event Handler Types

Always use proper event types from React. This provides autocomplete for event properties:

import type { ChangeEvent, FormEvent, MouseEvent } from "react";

export function Form() {
  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value); // TypeScript knows this is an HTMLInputElement
  };

  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    // Form submission logic
  };

  const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
    console.log(e.clientX, e.clientY); // Mouse coordinates
  };

  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleChange} />
      <button onClick={handleClick}>Submit</button>
    </form>
  );
}
Common Mistake

Don't use Event directly. Always use the specific event type like ChangeEvent, FormEvent, or MouseEvent with the element type.

Discriminated Unions

Use discriminated unions for variant props. This is the most powerful TypeScript pattern for React:

type AlertVariant = "info" | "warning" | "error" | "success";

interface AlertProps {
  variant: AlertVariant;
  title?: string;
  children: React.ReactNode;
  onClose?: () => void;
}

const alertStyles: Record<AlertVariant, string> = {
  info: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-100",
  warning: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-100",
  error: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-100",
  success: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100",
};

const alertIcons: Record<AlertVariant, React.ReactNode> = {
  info: <Info className="h-4 w-4" />,
  warning: <AlertTriangle className="h-4 w-4" />,
  error: <XCircle className="h-4 w-4" />,
  success: <CheckCircle className="h-4 w-4" />,
};

export function Alert({ variant, title, children, onClose }: AlertProps) {
  return (
    <div className={`p-4 rounded-lg ${alertStyles[variant]} relative`}>
      <div className="flex items-start gap-2">
        {alertIcons[variant]}
        <div className="flex-1">
          {title && <div className="font-semibold">{title}</div>}
          <div className="text-sm">{children}</div>
        </div>
        {onClose && (
          <button onClick={onClose} className="opacity-70 hover:opacity-100">
            <X className="h-4 w-4" />
          </button>
        )}
      </div>
    </div>
  );
}

Utility Types

Leverage TypeScript's built-in utility types. They save time and reduce verbosity:

// Make all props optional
type PartialUser = Partial<User>;

// Pick specific properties
type UserSummary = Pick<User, "id" | "name" | "email">;

// Omit specific properties (useful for create forms)
type CreateUserInput = Omit<User, "id" | "createdAt" | "updatedAt">;

// Make specific properties readonly
type ReadonlyConfig = Readonly<Config>;

// Extract return type from a function
type UserResponse = ReturnType<typeof fetchUser>;

TypeScript Pro Tips

Strict Mode Configuration

Always enable strict mode in your tsconfig.json:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitReturns": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  }
}
You're All Set!

Following these TypeScript patterns will help you write more maintainable, type-safe React applications. Your future self will thank you!

Previous

Understanding the Next.js App Router

Hien Phan
Hien Phan

Indie maker building in public. Ship fast, learn loud.

Navigation

  • Blog
  • Projects
  • About

Contact

harrisonphan5@gmail.com

Based in Vietnam 🇻🇳

© 2026 Hien Phan. All rights reserved.

Built withNext.js+Vercel