Special Sponsor:PromptBuilder— Fast, consistent prompt creation powered by 1,000+ expert templates.
Make your Product visible here.Contact Us

Home/TypeScript with React

TypeScript with React: Complete Guide

Typing React components correctly eliminates an entire class of prop bugs and makes your IDE autocomplete genuinely useful. This guide covers every pattern you will encounter — from simple props to generic components.

Typing Functional Components

The simplest approach is to define a props interface and pass it directly to the function parameter. Avoid React.FC — it is no longer recommended and was removed from many starter templates.

import React from "react";

// Define props as an interface
interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;        // optional
  variant?: "primary" | "secondary" | "ghost";
}

// Type the function parameter directly — no React.FC needed
function Button({ label, onClick, disabled = false, variant = "primary" }: ButtonProps) {
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      className={`btn btn-${variant}`}
    >
      {label}
    </button>
  );
}

// Usage — TypeScript errors if label or onClick is missing
<Button label="Save" onClick={() => console.log("saved")} />
<Button label="Cancel" onClick={handleCancel} variant="ghost" />

Props: interface vs type

Both interface and type work for props. Use type when you need union types or computed props.

// interface — extendable, conventional for object shapes
interface CardProps {
  title: string;
  description: string;
  imageUrl?: string;
}

// Extend for a special variant
interface FeatureCardProps extends CardProps {
  badge: string;
  badgeColor: "green" | "blue" | "red";
}

// type — required when props include union or conditional logic
type AlertProps =
  | { type: "success"; message: string }
  | { type: "error"; message: string; code: number }
  | { type: "info"; message: string; dismissible?: boolean };

function Alert(props: AlertProps) {
  if (props.type === "error") {
    // TypeScript knows 'code' is available here
    return <div>Error {props.code}: {props.message}</div>;
  }
  return <div>{props.message}</div>;
}

useState with TypeScript

TypeScript infers state type from the initial value. When the initial value is null or undefined, you must provide the generic argument.

import { useState } from "react";

interface User {
  id: number;
  name: string;
  email: string;
}

function UserProfile() {
  // Inferred from initial value — no annotation needed
  const [count, setCount] = useState(0);          // number
  const [name, setName]   = useState("");          // string
  const [active, setActive] = useState(false);    // boolean

  // Must specify type when initial value is null
  const [user, setUser] = useState<User | null>(null);

  // Array of objects — infer from initial value or specify explicitly
  const [users, setUsers] = useState<User[]>([]);

  // Object state
  const [form, setForm] = useState({ email: "", password: "" });
  // setForm({ email: "a@b.com", password: "secret" }); ✓
  // setForm({ email: "a@b.com" });                      ✗ missing password

  return <div>{user?.name}</div>;
}

useRef with TypeScript

useRef has two distinct uses: holding a DOM node and holding a mutable value. They are typed differently.

import { useRef, useEffect } from "react";

function SearchInput() {
  // DOM ref — initialize with null, TypeScript enforces null check
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    // Must check .current before accessing — it starts as null
    inputRef.current?.focus();
  }, []);

  return <input ref={inputRef} type="search" />;
}

function Timer() {
  // Mutable ref — no null, holds a timer ID
  const timerId = useRef<ReturnType<typeof setInterval>>(null!);

  function start() {
    timerId.current = setInterval(() => console.log("tick"), 1000);
  }

  function stop() {
    clearInterval(timerId.current);
  }

  return (
    <div>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  );
}

Typing Event Handlers

React ships with typed event interfaces for every DOM event. The element type is the generic argument.

import React from "react";

function Form() {
  // ChangeEvent<HTMLInputElement> for text inputs
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value); // string — fully typed
  };

  // ChangeEvent<HTMLSelectElement> for dropdowns
  const handleSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
    console.log(e.target.value);
  };

  // FormEvent<HTMLFormElement> for form submission
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    // process form data
  };

  // MouseEvent<HTMLButtonElement> for button clicks
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    console.log(e.clientX, e.clientY); // mouse position
  };

  // KeyboardEvent<HTMLInputElement> for key presses
  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === "Enter") console.log("submitted");
  };

  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleChange} onKeyDown={handleKeyDown} />
      <select onChange={handleSelect}><option>A</option></select>
      <button type="button" onClick={handleClick}>Click me</button>
    </form>
  );
}

Typing Children

import React from "react";

// ReactNode accepts any valid JSX child
interface WrapperProps {
  children: React.ReactNode;
  className?: string;
}

function Card({ children, className = "" }: WrapperProps) {
  return <div className={`card ${className}`}>{children}</div>;
}

// ReactElement accepts only JSX elements (not strings/numbers)
interface IconButtonProps {
  icon: React.ReactElement;
  label: string;
  onClick: () => void;
}

function IconButton({ icon, label, onClick }: IconButtonProps) {
  return (
    <button onClick={onClick} aria-label={label}>
      {icon}
      <span>{label}</span>
    </button>
  );
}

// PropsWithChildren utility — adds children automatically
import { PropsWithChildren } from "react";

function Section({ children, title }: PropsWithChildren<{ title: string }>) {
  return (
    <section>
      <h2>{title}</h2>
      {children}
    </section>
  );
}

Generic React Components

Components can be generic just like functions. This is especially useful for list, table, and select components that work with any data type.

// A type-safe list component — works with any item type
interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
  keyExtractor: (item: T) => string | number;
  emptyMessage?: string;
}

function List<T>({ items, renderItem, keyExtractor, emptyMessage = "No items" }: ListProps<T>) {
  if (items.length === 0) return <p>{emptyMessage}</p>;
  return (
    <ul>
      {items.map((item, i) => (
        <li key={keyExtractor(item)}>{renderItem(item, i)}</li>
      ))}
    </ul>
  );
}

// Usage — T is inferred from 'items'
<List
  items={users}
  keyExtractor={(u) => u.id}
  renderItem={(u) => <span>{u.name}</span>}
/>

Frequently Asked Questions

Should I use interface or type for React component props?

Both work. The React community conventionally uses 'interface' because it is extendable and produces clearer error messages. Use 'type' when you need unions, intersections, or computed types in your props.

How do I type useState in TypeScript?

TypeScript usually infers the state type from the initial value. When you initialize with null (common for async data), provide the type explicitly: useState<User | null>(null). The setter will also be typed correctly.

How do I type a React event handler in TypeScript?

Use React.ChangeEvent<HTMLInputElement> for input onChange, React.MouseEvent<HTMLButtonElement> for button onClick, React.FormEvent<HTMLFormElement> for form onSubmit. These give full autocomplete on the event object.

What is React.FC and should I use it?

React.FC is no longer recommended. It added implicit children to props (removed in React 18's types) and does not add real value. The modern approach: define props with an interface and type the function directly as function MyComponent(props: MyProps) {}.

How do I type useRef in TypeScript?

For DOM refs, initialize with null: useRef<HTMLInputElement>(null). For mutable values like a timer ID: useRef<number>(0). TypeScript enforces null checks for DOM refs before accessing .current.

How do I type the children prop in TypeScript React?

Use React.ReactNode as the children type. It accepts strings, numbers, JSX elements, arrays, fragments, and null/undefined. If you only want JSX elements, use React.ReactElement instead.

Convert your React JS to React TSX

Paste a JavaScript React component and get back properly typed TypeScript — props interfaces, hook types, and event handlers included.

Convert now →

From the blog

View all →