Skip to content

Join the Seedly owners community →

TypeScript

TypeScript in React

Typing React components, props, state, and events

Written by 14 min read1 activity
Pixl, your presenter

Pixl presents

Untyped props are how a component quietly gets a number where it wanted a string. Type your props, state and events.Untyped props are how a component quietly gets a number where it wanted a string. Type your props, state and events.

Pixl holds a mismatched brass plug up to a wooden wall socket, the right plug in his other paw
Typed props make sure a component gets the right shape

Ever passed the wrong prop to a React component and then burned 20 minutes staring at a blank screen wondering why nothing shows up? Yeah... TypeScript is about to be your new best friend.

It catches prop mistakes the second you make them, autocompletes the props a component takes while you type, and tells you what each component needs without you digging through its source code.

Typing Component Props#

The thing you'll do MOST with TypeScript in React is type your component's props.

// Define what props the component accepts
interface GreetingProps {
  name: string;
  age: number;
  isVIP?: boolean;  // Optional prop
}
 
// Use the interface to type the component
function Greeting({ name, age, isVIP }: GreetingProps) {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>Age: {age}</p>
      {isVIP && <span>VIP Member</span>}
    </div>
  );
}
 
// TypeScript checks props when you use the component
<Greeting name="Alice" age={28} />          // OK
<Greeting name="Bob" age="thirty" />         // Error! string not number
<Greeting name="Charlie" />                  // Error! missing 'age'

Common Prop Types#

These are the prop types you'll reach for most.

interface ComponentProps {
  // Basic types
  title: string;
  count: number;
  isActive: boolean;
 
  // Arrays and objects
  items: string[];
  user: { name: string; email: string };
 
  // Functions
  onClick: () => void;                    // No args, no return
  onChange: (value: string) => void;       // One arg, no return
  onSubmit: (data: FormData) => Promise<void>;  // Async
 
  // React-specific types
  children: React.ReactNode;              // Any renderable content
  style?: React.CSSProperties;           // Inline styles
  className?: string;                     // CSS classes
 
  // Union types for variants
  variant: "primary" | "secondary" | "danger";
  size: "sm" | "md" | "lg";
}

Children Props#

The children prop lets a component wrap around other content.

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>
  );
}
 
// Usage - children is whatever goes between the tags
<Card title="Welcome">
  <p>This is the card content.</p>
  <button>Click me</button>
</Card>

Typing State with useState#

TypeScript can usually guess your state types, but every now and then you have to spell it out.

import { useState } from 'react';
 
function UserProfile() {
  // TypeScript infers these automatically
  const [name, setName] = useState("Alice");        // string
  const [count, setCount] = useState(0);            // number
  const [isOpen, setIsOpen] = useState(false);      // boolean
 
  // Explicit type needed when initial value doesn't match future values
  const [user, setUser] = useState<User | null>(null);
  const [items, setItems] = useState<string[]>([]);
  const [error, setError] = useState<string | undefined>(undefined);
 
  // Later...
  setUser({ name: "Alice", email: "[email protected]" }); // OK
  setItems(["apple", "banana"]);  // OK
}
 
interface User {
  name: string;
  email: string;
}

Typing Events#

React events have their own specific types. Here are the ones you'll see the most.

function FormExample() {
  // Input change event
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  };
 
  // Form submit event (older code uses React.FormEvent here, which current React types mark as deprecated)
  const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
    e.preventDefault();
    // Process form data
  };
 
  // Button click event
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    console.log("Button clicked at", e.clientX, e.clientY);
  };
 
  // Keyboard event
  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === "Enter") {
      console.log("Enter pressed!");
    }
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <input
        onChange={handleChange}
        onKeyDown={handleKeyDown}
      />
      <button onClick={handleClick}>Submit</button>
    </form>
  );
}

Generic Components#

Pixl places an empty display stand with a carved teacup outline, a teacup waiting on a shelf behind
Give state a type when it starts empty and fills in later

You can build components that work with any kind of data.

// A generic list component
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}
 
function List<T>({ items, renderItem }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}
 
// Usage - TypeScript infers the type from items
interface User {
  id: number;
  name: string;
}
 
<List
  items={[{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]}
  renderItem={(user) => <span>{user.name}</span>}
/>
// TypeScript knows 'user' is User in renderItem

Typing Refs#

Refs need a type parameter for whichever DOM element they point at.

import { useRef } from 'react';
 
function InputFocus() {
  const inputRef = useRef<HTMLInputElement>(null);
 
  const focusInput = () => {
    // Need to check if ref is attached
    inputRef.current?.focus();
  };
 
  return (
    <div>
      <input ref={inputRef} placeholder="Click button to focus" />
      <button onClick={focusInput}>Focus Input</button>
    </div>
  );
}

Extending HTML Element Props#

Sometimes you want a component that takes every prop a normal HTML element takes, plus a few of your own.

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant: "primary" | "secondary";
  isLoading?: boolean;
}
 
function Button({ variant, isLoading, children, ...rest }: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant}`}
      disabled={isLoading}
      {...rest}
    >
      {isLoading ? "Loading..." : children}
    </button>
  );
}
 
// Accepts all normal button props plus your custom ones
<Button variant="primary" onClick={() => {}} type="submit">
  Save
</Button>

TL;DR#

  • Describe component props with interfaces, like interface Props { name: string }
  • Use React.ReactNode as the type for the children prop
  • useState figures out types by itself, but it needs an explicit type when the starting value is null
  • Event handlers have specific types, like React.ChangeEvent<HTMLInputElement>
  • Generic components (<T>) give you reusable, type-safe building blocks
  • Extend HTML attributes to make souped-up versions of native elements

What's Next?#

And that's the TypeScript module, donezo! You've covered types, interfaces, generics, union types, utility types, project setup, and React. That's a LOT (look at you go).

Real talk though, reading about it only gets you so far. Three weeks of poking at this stuff won't make you an expert (and anybody claiming expert status after three weeks of Claude Code deserves a raised eyebrow). Practice is what makes it stick. Try converting a small JavaScript project to TypeScript, or start your next project in TypeScript from day one...

This lesson ends with a short activity.