TypeScriptTypeScript with React

TypeScript with React

TypeScript and React are a powerful combination. TypeScript catches prop mismatches, incorrect event handlers, and missing context values at compile time rather than at runtime. This page covers the essential patterns every React developer needs — from typing basic components to advanced patterns like generic components and forwardRef.

Setting Up

The fastest way to start a TypeScript React project is with Vite or Create React App:

Bash
# Vite (recommended)
npm create vite@latest my-app -- --template react-ts
cd my-app && npm install && npm run dev

# Next.js (full-stack)
npx create-next-app@latest my-app --typescript

For an existing project, install the type definitions:

Bash
npm install --save-dev @types/react @types/react-dom
Note
React 18+ ships its own types in the main package. You only need @types/react separately if you are on an older version.
Function Components

The preferred way to type a function component is to define a Props interface and type the function parameter directly — no React.FC needed:

TSX
interface ButtonProps {
  label:    string;
  onClick:  () => void;
  disabled?: boolean;
  variant?: 'primary' | 'secondary' | 'danger';
}

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

// Usage — TypeScript validates every prop
<Button label="Save" onClick={() => console.log('saved')} />
<Button label="Delete" onClick={handleDelete} variant="danger" />
<Button label="Bad" onClick="not a function" />  // Error

Why avoid React.FC?

React.FC (or React.FunctionComponent) adds an implicit children prop and complicates typing. Since React 18 removed the implicit children, the direct approach is both cleaner and more precise:

TSX
// Avoid — React.FC adds historical baggage
const BadButton: React.FC<ButtonProps> = ({ label }) => <button>{label}</button>;

// Prefer — explicit, readable, no implicit children
function GoodButton({ label }: ButtonProps) {
  return <button>{label}</button>;
}
Typing Children

Children must be typed explicitly in React 18+. Use React.ReactNode for any renderable content:

TSX
import { ReactNode } from 'react';

interface CardProps {
  title: string;
  children: ReactNode;
  footer?: ReactNode;
}

function Card({ title, children, footer }: CardProps) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card__body">{children}</div>
      {footer && <div className="card__footer">{footer}</div>}
    </div>
  );
}

// Accepts any renderable content
<Card title="Hello">
  <p>Some text</p>
  <img src="/photo.jpg" alt="Photo" />
</Card>

Type

Accepts

ReactNode

Anything renderable: elements, strings, numbers, null, arrays

ReactElement

Only JSX elements (not strings or null)

ReactChild

Elements, strings, numbers (deprecated in React 18)

JSX.Element

A single JSX element — identical to ReactElement in most cases

Event Handler Types

React provides typed event objects for every DOM event. The pattern is React.XxxEvent<HTMLElement>:

TSX
import { ChangeEvent, FormEvent, MouseEvent, KeyboardEvent } from 'react';

function Form() {
  function handleInputChange(e: ChangeEvent<HTMLInputElement>) {
    console.log(e.target.value);     // string
    console.log(e.target.checked);   // boolean (for checkboxes)
  }

  function handleSelectChange(e: ChangeEvent<HTMLSelectElement>) {
    console.log(e.target.value);     // selected option value
  }

  function handleSubmit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const form = e.currentTarget;    // HTMLFormElement
  }

  function handleClick(e: MouseEvent<HTMLButtonElement>) {
    console.log(e.clientX, e.clientY);
  }

  function handleKeyDown(e: KeyboardEvent<HTMLInputElement>) {
    if (e.key === 'Enter') console.log('Enter pressed');
  }

  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleInputChange} onKeyDown={handleKeyDown} />
      <select onChange={handleSelectChange}>
        <option value="a">A</option>
      </select>
      <button type="submit" onClick={handleClick}>Submit</button>
    </form>
  );
}
Tip
Inline event handlers infer their types automatically from the JSX element. You only need to annotate the type when extracting the handler to a standalone function.
useRef for DOM Elements

TSX
import { useRef, useEffect } from 'react';

function AutoFocusInput() {
  // Pass the element type to useRef; start as null
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    // inputRef.current is HTMLInputElement | null
    inputRef.current?.focus();
  }, []);

  return <input ref={inputRef} placeholder="Auto-focused" />;
}

// For mutable values (not DOM refs), omit null and pass the initial value
function Timer() {
  const intervalRef = useRef<number>(0);  // no null — always a number

  useEffect(() => {
    intervalRef.current = window.setInterval(() => {
      console.log('tick');
    }, 1000);

    return () => clearInterval(intervalRef.current);
  }, []);

  return <div>Timer running</div>;
}
Note
Use useRef<T>(null) for DOM refs and useRef<T>(initialValue) for mutable values. The former gives T | null; the latter gives T.
Generic Components

Generic components are incredibly powerful — they let you write a single component that works with any data type while preserving full type safety:

TSX
interface ListProps<T> {
  items:      T[];
  renderItem: (item: T, index: number) => ReactNode;
  keyExtractor: (item: T) => string;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, i) => (
        <li key={keyExtractor(item)}>{renderItem(item, i)}</li>
      ))}
    </ul>
  );
}

// Usage — T is inferred from the items array
interface User { id: number; name: string; }

<List
  items={users}
  keyExtractor={(u) => String(u.id)}
  renderItem={(u) => <span>{u.name}</span>}
/>
forwardRef with TypeScript

forwardRef requires two type parameters: the ref type and the props type:

TSX
import { forwardRef, useImperativeHandle } from 'react';

interface InputProps {
  label:       string;
  placeholder?: string;
}

// forwardRef<RefType, PropsType>
const LabelledInput = forwardRef<HTMLInputElement, InputProps>(
  function LabelledInput({ label, placeholder }, ref) {
    return (
      <label>
        {label}
        <input ref={ref} placeholder={placeholder} />
      </label>
    );
  }
);

// Usage
function Parent() {
  const inputRef = useRef<HTMLInputElement>(null);

  return (
    <>
      <LabelledInput ref={inputRef} label="Name" placeholder="Enter name" />
      <button onClick={() => inputRef.current?.focus()}>Focus</button>
    </>
  );
}
Exposing Imperative API with useImperativeHandle

TSX
import { forwardRef, useImperativeHandle, useRef } from 'react';

interface VideoPlayerHandle {
  play:  () => void;
  pause: () => void;
  seek:  (seconds: number) => void;
}

interface VideoPlayerProps {
  src: string;
}

const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
  function VideoPlayer({ src }, ref) {
    const videoRef = useRef<HTMLVideoElement>(null);

    useImperativeHandle(ref, () => ({
      play:  () => videoRef.current?.play(),
      pause: () => videoRef.current?.pause(),
      seek:  (s) => { if (videoRef.current) videoRef.current.currentTime = s; },
    }));

    return <video ref={videoRef} src={src} />;
  }
);

function App() {
  const playerRef = useRef<VideoPlayerHandle>(null);

  return (
    <>
      <VideoPlayer ref={playerRef} src="/movie.mp4" />
      <button onClick={() => playerRef.current?.play()}>Play</button>
      <button onClick={() => playerRef.current?.seek(30)}>Skip 30s</button>
    </>
  );
}
Polymorphic Components

A polymorphic component renders as different HTML elements based on an as prop — fully type-safe:

TSX
import { ElementType, ComponentPropsWithoutRef } from 'react';

type PolymorphicProps<T extends ElementType> = {
  as?: T;
} & ComponentPropsWithoutRef<T>;

function Text<T extends ElementType = 'p'>({
  as,
  children,
  ...rest
}: PolymorphicProps<T>) {
  const Component = as ?? 'p';
  return <Component {...rest}>{children}</Component>;
}

// Renders as <p>
<Text>Hello world</Text>

// Renders as <h1> — gets heading-specific props
<Text as="h1">Page Title</Text>

// Renders as <a> — gets href and target props
<Text as="a" href="https://example.com" target="_blank">Link</Text>

// Error: divs don't have href
<Text as="div" href="/bad" />  // Error
Typing Higher-Order Components

TSX
import { ComponentType, ReactNode } from 'react';

interface WithAuthProps {
  user: { id: string; role: string };
}

function withAuth<P extends object>(
  WrappedComponent: ComponentType<P & WithAuthProps>
) {
  return function AuthenticatedComponent(props: Omit<P, keyof WithAuthProps>) {
    const user = useCurrentUser(); // hypothetical hook

    if (!user) return <div>Please log in</div>;

    return <WrappedComponent {...(props as P)} user={user} />;
  };
}

interface DashboardProps extends WithAuthProps {
  title: string;
}

function Dashboard({ user, title }: DashboardProps) {
  return <div>Welcome, {user.id} — {title}</div>;
}

const AuthDashboard = withAuth(Dashboard);

// user prop is injected — only title is required
<AuthDashboard title="My Dashboard" />
Component Prop Types Reference

Utility

What it extracts

ComponentProps<"button">

All props a native button element accepts

ComponentPropsWithoutRef<"input">

Native input props without the ref prop

ComponentPropsWithRef<"div">

Native div props including the ref prop

React.HTMLAttributes<HTMLElement>

Generic HTML element attributes

React.AriaAttributes

All ARIA attributes

Key Takeaways
  1. Type props as an interface parameter directly — avoid React.FC

  2. Use ReactNode for children; ReactElement when you need only JSX elements

  3. Event handlers use React.ChangeEvent<T>, React.MouseEvent<T>, etc.

  4. useRef<HTMLElement>(null) for DOM refs; useRef<T>(value) for mutable values

  5. Generic components infer T from usage — write ListProps<T> with a generic function

  6. forwardRef takes two type params: forwardRef<RefType, PropsType>

  7. useImperativeHandle exposes a custom imperative API through a ref

  8. Polymorphic components use ElementType and ComponentPropsWithoutRef for full safety