Chapter 10.1☕ 18 min read

TS with React (Props & State)

React components with TypeScript — every prop, state, and event is checked at the gate.

01The Component Quality Check

React and TypeScript are a perfect match. React's component-based architecture maps naturally to TypeScript's type system — every component has a clear contract of what props it accepts, what state it manages, and what events it handles. TypeScript enforces these contracts at compile time, catching mismatches before they reach the browser.

Think of a Charminar-level quality check for your components. Every component is like a shop in the Laad Bazaar around Charminar — each shop has a specific inventory, serves specific customers, and operates in a specific way. If a customer walks in expecting bangles at a shop that only sells pearls, there's a mismatch — the shop's "props" and the customer's "expectations" don't align.

TypeScript is like having a quality inspector at every shop's entrance. The inspector checks that the customer brings the right items (props), understands what the shop offers (component API), and doesn't ask for things that don't exist (invalid state). Before the transaction happens, the inspector catches the mismatch. No runtime crashes, no confused customers, no broken workflows.

In this chapter, we'll cover the essential React + TypeScript patterns: typing props (the shop inventory), typing state (what's in the back room), typing events (customer interactions), and typing hooks (the shop's tools). Once you learn these patterns, every component you write will be self-documenting, type-safe, and impossible to misuse — like a well-organized shop in Hyderabad's most famous market.

02Typing Props

The foundation of any React + TypeScript component is the props interface. This defines the contract between the parent component (who uses the component) and the child component (who renders the UI).

Basic Props Interface

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

// Define the component
const Button: React.FC<ButtonProps> = ({
  label,
  onClick,
  disabled = false,
  variant,
}) => {
  const className =
    `btn btn-${variant}`;

  return (
    <button
      className={className}
      onClick={onClick}
      disabled={disabled}
    >
      {label}
    </button>
  );
};

Alternative: Direct Prop Annotation

// Some teams prefer this style
// (no implicit children prop)
function Button({
  label,
  onClick,
  variant,
}: ButtonProps) {
  return (
    <button onClick={onClick}>
      {label}
    </button>
  );
}

Children Prop

interface CardProps {
  title: string;
  children: React.ReactNode;
}

// children can be:
// string, number, JSX, arrays, fragments
// ReactNode covers all of these

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

Generic Components

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

function List<T>({
  items,
  renderItem
}: ListProps<T>) {
  return (
    <ul>
      {items.map(renderItem)}
    </ul>
  );
}

// Usage — T is inferred!
<List
  items={users}
  renderItem={(user) =>
    <li>{user.name}</li>
  }
/>
03Typing State & Events

TypeScript also provides full type safety for React state management and event handling.

useState with TypeScript

// Type is INFERRED from initial value
const [count, setCount] = useState(0);
// count is number, setCount expects number

const [name, setName] = useState("");
// name is string

const [isActive, setIsActive] =
  useState(false);
// isActive is boolean

// EXPLICIT type for complex state
interface User {
  id: number;
  name: string;
}

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

const [items, setItems] =
  useState<string[]>([]);
// Explicit for empty arrays

useReducer with TypeScript

type Action =
  | { type: "increment" }
  | { type: "decrement" }
  | { type: "reset" }
  | { type: "set"; payload: number };

function reducer(
  state: number,
  action: Action
): number {
  switch (action.type) {
    case "increment":
      return state + 1;
    case "decrement":
      return state - 1;
    case "reset":
      return 0;
    case "set":
      return action.payload;
  }
}

const [state, dispatch] =
  useReducer(reducer, 0);

Event Handlers

// Click events
const handleClick = (
  e: React.MouseEvent<
    HTMLButtonElement
  >
) => {
  console.log(e.clientX, e.clientY);
};

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

// Form submit
const handleSubmit = (
  e: React.FormEvent<
    HTMLFormElement
  >
) => {
  e.preventDefault();
  // Process form data
};

useRef with TypeScript

// Mutable ref (no initial value)
const intervalRef =
  useRef<number | null>(null);

// DOM element ref
const inputRef =
  useRef<HTMLInputElement>(null!);
// Use null! only when you know
// the element exists on mount

// Safer approach:
const inputRef =
  useRef<HTMLInputElement>(null);

useEffect(() => {
  inputRef.current?.focus();
  // Optional chaining handles null
}, []);
04React + TS Traps

React + TypeScript has some common pitfalls. Let's walk through the most important ones to avoid.

Trap 1: Not handling null state

const [user, setUser] =
  useState<User>(null!);
// null! lies to TypeScript!
// At runtime, user might be null

// CORRECT:
const [user, setUser] =
  useState<User | null>(null);
if (user) {
  console.log(user.name);
}

Trap 2: Over-typing with React.FC

React.FC includes implicit children. If your component doesn't accept children, prefer direct prop annotation to be explicit.

Trap 3: Using typeof useState incorrectly

// This does NOT work:
const [items, setItems] =
  useState([]);
// items is never[] — empty array type!

// Must specify:
const [items, setItems] =
  useState<string[]>([]);

Trap 4: Forgetting that setState with objects needs spread

const [form, setForm] =
  useState({ name: "", email: "" });

// setState does NOT merge!
// Must spread manually:
setForm(prev => ({
  ...prev,
  name: "Imran",
}));
05React + TypeScript Cheatsheet

Here's your complete cheatsheet for React + TypeScript!

Component Types:

interface Props { name: string; }
const Comp: React.FC<Props> = ({ name }) => ...
function Comp({ name }: Props) { ... }
function Comp<T>({ items }: { items: T[] }) => ...

Hooks:

useState<T>(initial)
useReducer<S, A>(reducer, initial)
useRef<T>(initial)
useContext<T>(context)

Events:

React.MouseEvent<E>
React.ChangeEvent<E>
React.FormEvent<E>
React.KeyboardEvent<E>

Key Rules:

  • Always define a props interface for every component — this is the contract
  • Handle null/undefined in state explicitly with union types
  • Use React event types (not DOM event types) for handlers
  • Specify generic type for empty arrays: useState<string[]>([])
  • Use useReducer for complex state logic with discriminated unions

The Golden Rule: "React + TypeScript is like a Charminar-level quality check. Every prop is inspected, every state is verified, every event is validated. The compiler catches mismatches before they reach your users. Treat your components like Laad Bazaar shops — clear contracts, consistent products, and zero surprises, bhai!"

Key Takeaways

  • Define a props interface for every component — it serves as the component contract
  • useState infers type from initial value; use explicit type for null or empty array initial values
  • Use React synthetic event types (React.MouseEvent, etc.) instead of DOM event types
  • useReducer with discriminated unions provides type-safe state management
  • Always use optional chaining (?.) or null checks for optional props and nullable state
  • Prefer direct prop annotation over React.FC to avoid implicit children prop
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase