Back to Blog
TypeScriptReactBest PracticesWeb Development

TypeScript Tips for React Developers

3 min read
TypeScript Tips for React Developers

TypeScript has become an essential tool in modern React development. It catches bugs early, improves developer experience with autocomplete, and makes refactoring safer. Let's explore some advanced TypeScript patterns that will level up your React code.

Proper Component Typing

Functional Components

The best way to type functional components has evolved:

// ✅ Recommended
type ButtonProps = {
  label: string
  onClick: () => void
  variant?: 'primary' | 'secondary'
}

function Button({ label, onClick, variant = 'primary' }: ButtonProps) {
  return (
    <button onClick={onClick} className={variant}>
      {label}
    </button>
  )
}

Props with Children

Handle children properly with React.ReactNode:

type CardProps = {
  title: string
  children: React.ReactNode
}

function Card({ title, children }: CardProps) {
  return (
    <div>
      <h2>{title}</h2>
      {children}
    </div>
  )
}

Advanced Patterns

Generic Components

Create reusable components with generics:

type SelectProps<T> = {
  options: T[]
  value: T
  onChange: (value: T) => void
  getLabel: (option: T) => string
}

function Select<T>({ options, value, onChange, getLabel }: SelectProps<T>) {
  return (
    <select value={String(value)} onChange={(e) => onChange(options[Number(e.target.value)])}>
      {options.map((option, index) => (
        <option key={index} value={index}>
          {getLabel(option)}
        </option>
      ))}
    </select>
  )
}

Discriminated Unions

Model complex state with discriminated unions:

type LoadingState = { status: 'loading' }
type ErrorState = { status: 'error'; error: string }
type SuccessState<T> = { status: 'success'; data: T }

type AsyncState<T> = LoadingState | ErrorState | SuccessState<T>

function DataDisplay<T>({ state }: { state: AsyncState<T> }) {
  switch (state.status) {
    case 'loading':
      return <Spinner />
    case 'error':
      return <Error message={state.error} />
    case 'success':
      return <Data data={state.data} />
  }
}

Hooks Typing

useState with Complex Types

type User = {
  id: string
  name: string
  email: string
}

// TypeScript infers the type
const [user, setUser] = useState<User | null>(null)

Custom Hooks

type UseToggleReturn = {
  isOn: boolean
  toggle: () => void
  setOn: () => void
  setOff: () => void
}

function useToggle(initialValue = false): UseToggleReturn {
  const [isOn, setIsOn] = useState(initialValue)

  return {
    isOn,
    toggle: () => setIsOn((prev) => !prev),
    setOn: () => setIsOn(true),
    setOff: () => setIsOn(false),
  }
}

Event Handling

Type events properly for better type safety:

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

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" onChange={handleChange} />
      <button type="submit">Submit</button>
    </form>
  )
}

Utility Types

Leverage TypeScript's built-in utility types:

type User = {
  id: string
  name: string
  email: string
  password: string
}

// Pick specific properties
type UserProfile = Pick<User, 'id' | 'name' | 'email'>

// Omit sensitive data
type PublicUser = Omit<User, 'password'>

// Make all properties optional
type PartialUser = Partial<User>

// Make all properties required
type RequiredUser = Required<User>

Best Practices

  1. Avoid any: Use unknown when you're unsure of a type
  2. Use strict mode: Enable "strict": true in tsconfig.json
  3. Prefer interfaces for objects: They're more extensible
  4. Use type for unions: type Status = 'idle' | 'loading' | 'error'
  5. Leverage type inference: Don't over-annotate

Conclusion

TypeScript transforms the React development experience by catching errors before runtime and providing excellent IDE support. These patterns will help you write more robust, maintainable code.

Start small, gradually introduce these patterns into your codebase, and watch your confidence in refactoring grow. The upfront investment in proper typing pays dividends in the long run.