TypeScript React Components

Med

TypeScript and React are a natural pairing. Typing component props ensures that consumers pass the correct data, and TypeScript catches missing or wrong props at compile time. Understanding how to type functional components, children, event handlers, and common React patterns is essential for any modern React project.

Interactive Visualization

Key Points

  • Define props as an interface: `interface ButtonProps { label: string; onClick: () => void }`
  • Use `React.FC<Props>` sparingly — prefer explicit return types or no annotation
  • Type children with `React.ReactNode` for the broadest JSX-compatible type
  • Event handlers use React synthetic event types: `React.MouseEvent<HTMLButtonElement>`
  • Discriminated union props enable type-safe component variants
  • Use `React.ComponentPropsWithoutRef<"button">` to inherit native HTML props
  • Generic components use generics in the function signature for type-safe data rendering

Code Examples

Component Props & Events

interface ButtonProps {
  label: string
  variant?: 'primary' | 'secondary' | 'danger'
  disabled?: boolean
  onClick: (event: React.MouseEvent<HTMLButtonElement>) => void
}

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

// Extending native HTML props
type InputProps = React.ComponentPropsWithoutRef<'input'> & {
  label: string
  error?: string
}

function Input({ label, error, ...rest }: InputProps) {
  return (
    <label>
      {label}
      <input {...rest} />
      {error && <span className="error">{error}</span>}
    </label>
  )
}

Props interfaces define the contract between a component and its consumers. Extending native HTML props with ComponentPropsWithoutRef avoids redefining standard attributes.

Children & Composition

// ReactNode is the broadest children type
interface CardProps {
  title: string
  children: React.ReactNode
}

function Card({ title, children }: CardProps) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-body">{children}</div>
    </div>
  )
}

// Render prop pattern with TypeScript
interface DataFetcherProps<T> {
  url: string
  children: (data: T, loading: boolean) => React.ReactNode
}

function DataFetcher<T>({ url, children }: DataFetcherProps<T>) {
  const [data, setData] = React.useState<T | null>(null)
  const [loading, setLoading] = React.useState(true)

  React.useEffect(() => {
    fetch(url)
      .then(r => r.json() as Promise<T>)
      .then(d => { setData(d); setLoading(false) })
  }, [url])

  return <>{data && children(data, loading)}</>
}

React.ReactNode accepts anything renderable: elements, strings, numbers, fragments, null. The render prop pattern benefits greatly from generics to type the data being passed.

Discriminated Union Props

// Props that depend on a variant discriminant
type AlertProps =
  | { variant: 'success'; message: string }
  | { variant: 'error'; message: string; retryAction: () => void }
  | { variant: 'loading' }

function Alert(props: AlertProps) {
  switch (props.variant) {
    case 'success':
      return <div className="alert-success">{props.message}</div>
    case 'error':
      return (
        <div className="alert-error">
          {props.message}
          <button onClick={props.retryAction}>Retry</button>
        </div>
      )
    case 'loading':
      return <div className="alert-loading">Loading...</div>
  }
}

// Usage
<Alert variant="error" message="Failed" retryAction={() => {}} />
// <Alert variant="loading" message="hi" />  // Error: message not on loading

Discriminated union props ensure that variant-specific props are only available when the matching variant is selected. TypeScript enforces this at compile time.

Common Mistakes

  • Using `React.FC` which adds implicit children and complicates generic components
  • Typing children as `JSX.Element` instead of `React.ReactNode` (too restrictive)
  • Forgetting to type event handlers — `onClick: () => void` misses the event parameter
  • Not using `ComponentPropsWithoutRef` when wrapping native HTML elements
  • Creating monolithic props interfaces instead of using discriminated unions for variants

Interview Tips

  • Know the difference between ReactNode, ReactElement, and JSX.Element
  • Explain why `React.FC` is often avoided in modern TypeScript React codebases
  • Demonstrate generic components — they show advanced TypeScript + React knowledge
  • Mention discriminated union props for type-safe component variants

Related Concepts