Back to articles
TypeScript

TypeScript Best Practices for React Developers

Essential TypeScript patterns and practices specifically for React development. Learn about proper typing, generics, utility types, and avoiding common pitfalls.

10 mins read
TypeScriptReactBest PracticesType Safety

TypeScript has become essential for building robust React applications. This guide covers essential TypeScript patterns and practices specifically for React development, including proper typing, generics, utility types, and common pitfalls to avoid.

Setting Up TypeScript for React

Start with a proper TypeScript configuration for React projects:

JSONtsconfig.json
{  "compilerOptions": {    "target": "es5",    "lib": ["dom", "dom.iterable", "es6"],    "allowJs": true,    "skipLibCheck": true,    "esModuleInterop": true,    "allowSyntheticDefaultImports": true,    "strict": true,    "forceConsistentCasingInFileNames": true,    "noFallthroughCasesInSwitch": true,    "module": "esnext",    "moduleResolution": "node",    "resolveJsonModule": true,    "isolatedModules": true,    "noEmit": true,    "jsx": "react-jsx"  },  "include": [    "src"  ]}

Typing React Components

Always use proper types for React components. Here are the most common patterns:

ReactComponentTypes.tsx
import React, { ReactNode, PropsWithChildren } from 'react';// Basic component props interfaceinterface ButtonProps {  variant: 'primary' | 'secondary' | 'danger';  size?: 'small' | 'medium' | 'large';  disabled?: boolean;  onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;  children: ReactNode;}// Using React.FC (Function Component) - optional but explicitexport const Button: React.FC<ButtonProps> = ({  variant,  size = 'medium',  disabled = false,  onClick,  children}) => {  return (    <button      className={`btn btn-${variant} btn-${size}`}      disabled={disabled}      onClick={onClick}    >      {children}    </button>  );};// Alternative: Direct function declaration (recommended)export function ButtonAlt({  variant,  size = 'medium',  disabled = false,  onClick,  children}: ButtonProps) {  return (    <button      className={`btn btn-${variant} btn-${size}`}      disabled={disabled}      onClick={onClick}    >      {children}    </button>  );}// Component with children helperinterface CardProps extends PropsWithChildren {  title: string;  footer?: ReactNode;}export function Card({ title, footer, children }: CardProps) {  return (    <div className="card">      <header>{title}</header>      <main>{children}</main>      {footer && <footer>{footer}</footer>}    </div>  );}

Advanced Props Patterns

Handle complex prop scenarios with discriminated unions and conditional types:

ReactAdvancedProps.tsx
import React from 'react';// Discriminated unions for mutually exclusive propstype InputProps = {  label: string;  name: string;  required?: boolean;} & (  | {      type: 'text' | 'email' | 'password';      placeholder?: string;    }  | {      type: 'number';      min?: number;      max?: number;    }  | {      type: 'select';      options: Array<{ value: string; label: string }>;    });export function Input(props: InputProps) {  const { label, name, required, type } = props;  return (    <div className="input-group">      <label htmlFor={name}>        {label} {required && '*'}      </label>            {type === 'select' ? (        <select name={name} required={required}>          {props.options.map(option => (            <option key={option.value} value={option.value}>              {option.label}            </option>          ))}        </select>      ) : (        <input          type={type}          name={name}          required={required}          {...(type === 'text' || type === 'email' || type === 'password'             ? { placeholder: props.placeholder }            : {})}          {...(type === 'number'             ? { min: props.min, max: props.max }            : {})}        />      )}    </div>  );}// Optional vs Required props patterninterface BaseModalProps {  isOpen: boolean;  onClose: () => void;  children: React.ReactNode;}interface ModalWithTitle extends BaseModalProps {  title: string;  subtitle?: string;}interface ModalWithoutTitle extends BaseModalProps {  title?: never;  subtitle?: never;}type ModalProps = ModalWithTitle | ModalWithoutTitle;export function Modal(props: ModalProps) {  if (!props.isOpen) return null;  return (    <div className="modal">      <div className="modal-content">        {props.title && (          <header>            <h2>{props.title}</h2>            {props.subtitle && <p>{props.subtitle}</p>}          </header>        )}        <main>{props.children}</main>      </div>    </div>  );}

Typing Hooks

Properly type React hooks for better type safety and IDE support:

ReactHookTypes.tsx
import React, { useState, useEffect, useRef, useCallback } from 'react';// State with explicit typesexport function useCounter(initialValue: number = 0) {  const [count, setCount] = useState<number>(initialValue);    const increment = useCallback(() => setCount(prev => prev + 1), []);  const decrement = useCallback(() => setCount(prev => prev - 1), []);  const reset = useCallback(() => setCount(initialValue), [initialValue]);    return { count, increment, decrement, reset } as const;}// Generic hook for API callsinterface ApiState<T> {  data: T | null;  loading: boolean;  error: string | null;}export function useApi<T>(url: string): ApiState<T> {  const [state, setState] = useState<ApiState<T>>({    data: null,    loading: true,    error: null  });  useEffect(() => {    let isMounted = true;    fetch(url)      .then(response => {        if (!response.ok) {          throw new Error(`HTTP error! status: ${response.status}`);        }        return response.json();      })      .then((data: T) => {        if (isMounted) {          setState({ data, loading: false, error: null });        }      })      .catch(error => {        if (isMounted) {          setState({ data: null, loading: false, error: error.message });        }      });    return () => {      isMounted = false;    };  }, [url]);  return state;}// Typed useRef examplesexport function FocusInput() {  const inputRef = useRef<HTMLInputElement>(null);  const divRef = useRef<HTMLDivElement>(null);  const focusInput = () => {    inputRef.current?.focus();  };  useEffect(() => {    if (divRef.current) {      console.log('Div height:', divRef.current.offsetHeight);    }  }, []);  return (    <div ref={divRef}>      <input ref={inputRef} type="text" />      <button onClick={focusInput}>Focus Input</button>    </div>  );}

Event Handling Types

TypeScript provides specific types for different HTML events:

ReactEventHandling.tsx
import React, { FormEvent, ChangeEvent, KeyboardEvent, MouseEvent } from 'react';interface FormData {  username: string;  email: string;  age: number;}export function ContactForm() {  const [formData, setFormData] = React.useState<FormData>({    username: '',    email: '',    age: 0  });  // Form submission  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {    event.preventDefault();    console.log('Form submitted:', formData);  };  // Input changes  const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {    const { name, value, type } = event.target;        setFormData(prev => ({      ...prev,      [name]: type === 'number' ? parseInt(value, 10) || 0 : value    }));  };  // Keyboard events  const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {    if (event.key === 'Enter') {      event.preventDefault();      // Handle Enter key    }  };  // Mouse events  const handleButtonClick = (event: MouseEvent<HTMLButtonElement>) => {    console.log('Button clicked at:', event.clientX, event.clientY);  };  return (    <form onSubmit={handleSubmit}>      <input        type="text"        name="username"        value={formData.username}        onChange={handleInputChange}        onKeyDown={handleKeyDown}        placeholder="Username"      />            <input        type="email"        name="email"        value={formData.email}        onChange={handleInputChange}        placeholder="Email"      />            <input        type="number"        name="age"        value={formData.age}        onChange={handleInputChange}        placeholder="Age"      />            <button type="submit" onClick={handleButtonClick}>        Submit      </button>    </form>  );}

Utility Types for React

Leverage TypeScript utility types to create flexible and reusable component interfaces:

ReactUtilityTypes.tsx
import React, { ComponentProps } from 'react';// Extract props from existing componentstype ButtonProps = ComponentProps<'button'>;type DivProps = ComponentProps<'div'>;// Extend native element propsinterface CustomButtonProps extends Omit<ButtonProps, 'type'> {  variant: 'primary' | 'secondary';  loading?: boolean;}export function CustomButton({   variant,   loading,   children,   disabled,  ...buttonProps }: CustomButtonProps) {  return (    <button      {...buttonProps}      disabled={disabled || loading}      className={`btn btn-${variant} ${loading ? 'loading' : ''}`}    >      {loading ? 'Loading...' : children}    </button>  );}// Pick specific propsinterface User {  id: number;  name: string;  email: string;  password: string;  role: 'admin' | 'user';  createdAt: Date;}// Only pick safe props for displaytype SafeUser = Pick<User, 'id' | 'name' | 'email' | 'role'>;// Or omit sensitive propstype PublicUser = Omit<User, 'password'>;interface UserCardProps {  user: SafeUser;  onEdit?: (userId: number) => void;}export function UserCard({ user, onEdit }: UserCardProps) {  return (    <div className="user-card">      <h3>{user.name}</h3>      <p>{user.email}</p>      <span className={`role role-${user.role}`}>{user.role}</span>      {onEdit && (        <button onClick={() => onEdit(user.id)}>          Edit        </button>      )}    </div>  );}// Partial for optional updatesinterface UpdateUserData extends Partial<Pick<User, 'name' | 'email' | 'role'>> {}function updateUser(id: number, updates: UpdateUserData) {  // Implementation here  console.log(`Updating user ${id} with:`, updates);}// Record for mapped typestype UserPermissions = Record<User['role'], string[]>;const permissions: UserPermissions = {  admin: ['read', 'write', 'delete'],  user: ['read']};

Generic Components

Create reusable components with generics for better type safety:

ReactGenericComponents.tsx
import React from 'react';// Generic list componentinterface ListProps<T> {  items: T[];  renderItem: (item: T, index: number) => React.ReactNode;  keyExtractor: (item: T) => string | number;  emptyMessage?: string;}export function List<T>({   items,   renderItem,   keyExtractor,   emptyMessage = 'No items found' }: ListProps<T>) {  if (items.length === 0) {    return <div className="empty-list">{emptyMessage}</div>;  }  return (    <ul className="list">      {items.map((item, index) => (        <li key={keyExtractor(item)}>          {renderItem(item, index)}        </li>      ))}    </ul>  );}// Usage exampleinterface Product {  id: number;  name: string;  price: number;}export function ProductList({ products }: { products: Product[] }) {  return (    <List      items={products}      keyExtractor={product => product.id}      renderItem={(product) => (        <div>          <h3>{product.name}</h3>          <p>${product.price}</p>        </div>      )}      emptyMessage="No products available"    />  );}// Generic form field componentinterface FormFieldProps<T extends string | number> {  name: string;  label: string;  value: T;  onChange: (name: string, value: T) => void;  type: T extends string ? 'text' | 'email' | 'password' : 'number';  required?: boolean;}export function FormField<T extends string | number>({  name,  label,  value,  onChange,  type,  required = false}: FormFieldProps<T>) {  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {    const newValue = type === 'number'       ? (parseInt(e.target.value, 10) as T)      : (e.target.value as T);        onChange(name, newValue);  };  return (    <div className="form-field">      <label htmlFor={name}>        {label} {required && '*'}      </label>      <input        id={name}        name={name}        type={type}        value={value}        onChange={handleChange}        required={required}      />    </div>  );}

Common TypeScript Pitfalls

⚠️

Avoid These Common Mistakes

Don't use `any` type - use `unknown` or proper types instead
Avoid function declarations inside JSX - define them outside or use useCallback
Don't ignore TypeScript errors - fix them properly
Use const assertions for literal types: `as const`
Don't over-engineer types - keep them simple and readable
ReactCommonMistakes.tsx
// ❌ Bad: Using anyfunction BadComponent({ data }: { data: any }) {  return <div>{data.someProperty}</div>;}// ✅ Good: Proper typinginterface ComponentData {  someProperty: string;  optional?: number;}function GoodComponent({ data }: { data: ComponentData }) {  return <div>{data.someProperty}</div>;}// ❌ Bad: Inline function declarationsfunction BadEventHandling() {  return (    <button onClick={(e) => {      // This creates a new function on every render      console.log('Clicked');    }}>      Click me    </button>  );}// ✅ Good: Proper event handlersfunction GoodEventHandling() {  const handleClick = React.useCallback((e: React.MouseEvent) => {    console.log('Clicked');  }, []);  return <button onClick={handleClick}>Click me</button>;}// ❌ Bad: Ignoring null/undefinedfunction BadNullHandling({ user }: { user: User | null }) {  return <div>{user.name}</div>; // TypeScript error!}// ✅ Good: Proper null checkingfunction GoodNullHandling({ user }: { user: User | null }) {  if (!user) {    return <div>No user found</div>;  }    return <div>{user.name}</div>;}// ✅ Alternative: Optional chainingfunction AlternativeNullHandling({ user }: { user: User | null }) {  return <div>{user?.name ?? 'No user found'}</div>;}

Best Practices Summary

Strict TypeScript

Enable strict mode in tsconfig.json for better type safety and error catching.

Interface First

Define interfaces before implementing components for better design and documentation.

Generic When Needed

Use generics for reusable components but don't over-engineer simple cases.

Utility Types

Leverage Pick, Omit, Partial, and other utility types for flexible interfaces.