REACT Lesson 42 – React with TypeScript | Dataplexa
LESSON 42

React with TypeScript

Add type safety to your React components and catch errors before they reach production

TypeScript transforms React development. No more wondering what props a component expects. No more silent runtime failures from typos. Type safety catches issues at build time. The DataFlow team discovered this the hard way. Their dashboard worked perfectly in testing, but crashed in production when the API returned unexpected data shapes. One missing property brought down the entire analytics view.

Why TypeScript with React

JavaScript is dynamically typed. Variables can be anything at any time. React components receive props that could be strings, numbers, objects, or undefined. TypeScript adds a type layer that prevents these surprises. Think of TypeScript as a spell-checker for your code. Just as spell-check catches typos before you send an email, TypeScript catches type errors before your app runs.

Benefits in React

Props get explicit types, state updates are validated, and your IDE provides accurate autocompletion. Refactoring becomes safe because TypeScript tracks every component that uses changed props.

Setting Up React with TypeScript

Modern React projects support TypeScript out of the box. Create React App, Vite, and Next.js all include TypeScript templates.
// Create new TypeScript React project
npx create-react-app dataflow-ts --template typescript

// Or with Vite (faster)
npm create vite@latest dataflow-ts -- --template react-ts

// Install types for existing project
npm install typescript @types/react @types/react-dom
The @types/react package provides TypeScript definitions for React. It tells TypeScript what useState, useEffect, and component props should look like.

Typing React Components

React components are just functions that return JSX. TypeScript can infer the return type, but you should explicitly type the props.
// Basic typed component for DataFlow stats
interface StatsCardProps {
  title: string;
  value: number;
  trend: 'up' | 'down' | 'stable';
}

function StatsCard({ title, value, trend }: StatsCardProps) {
  return (
    <div className="stats-card">
      <h3>{title}</h3>
      <p>${value.toLocaleString()}</p>
      <span className={`trend-${trend}`}>
        {trend === 'up' ? '↗' : trend === 'down' ? '↘' : '→'}
      </span>
    </div>
  );
}
DataFlow Dashboard

What just happened?

The interface defines exact prop types. trend: 'up' | 'down' | 'stable' creates a union type - only those three strings are allowed. Try this: pass an invalid trend value and watch TypeScript catch the error.

useState with Types

React hooks need type annotations when TypeScript can't infer the type. useState often infers correctly from initial values, but complex types require explicit annotation.
// DataFlow user management with typed state
interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'viewer';
}

function UserManager() {
  const [users, setUsers] = useState<User[]>([]);
  const [selectedUser, setSelectedUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(false);

  const addUser = (userData: Omit<User, 'id'>) => {
    const newUser: User = {
      id: Date.now(),
      ...userData
    };
    setUsers(prev => [...prev, newUser]);
  };

  return (
    <div>
      <h2>Users: {users.length}</h2>
      <button onClick={() => addUser({
        name: 'John Doe',
        email: 'john@dataflow.com',
        role: 'user'
      })}>
        Add User
      </button>
    </div>
  );
}
DataFlow Dashboard

What just happened?

Omit<User, 'id'> creates a type with all User properties except id. useState<User[]> tells TypeScript this state holds an array of User objects. Try this: hover over variables in VS Code to see their inferred types.

Event Handling Types

React events have specific TypeScript types. Button clicks get MouseEvent, form inputs get ChangeEvent. These types provide access to event properties with full autocomplete.
// DataFlow search with typed events
import { ChangeEvent, FormEvent, useState } from 'react';

function SearchBar() {
  const [query, setQuery] = useState('');
  const [filters, setFilters] = useState({
    category: '',
    dateRange: '30days'
  });

  const handleSearch = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    console.log('Searching for:', query);
  };

  const handleQueryChange = (e: ChangeEvent<HTMLInputElement>) => {
    setQuery(e.target.value);
  };

  const handleFilterChange = (e: ChangeEvent<HTMLSelectElement>) => {
    setFilters(prev => ({
      ...prev,
      [e.target.name]: e.target.value
    }));
  };

  return (
    <form onSubmit={handleSearch}>
      <input
        type="text"
        value={query}
        onChange={handleQueryChange}
        placeholder="Search transactions..."
      />
      <select name="category" onChange={handleFilterChange}>
        <option value="">All Categories</option>
        <option value="sales">Sales</option>
        <option value="marketing">Marketing</option>
      </select>
      <button type="submit">Search</button>
    </form>
  );
}
DataFlow Dashboard

Advanced TypeScript Patterns

Real applications need sophisticated type patterns. Generic components, conditional types, and mapped types solve complex scenarios that basic interfaces cannot handle.

Generic Components

Components that work with any data type while maintaining type safety

Utility Types

Pick, Omit, Partial transform existing types into new shapes

Conditional Types

Types that change based on conditions, like ternary operators for types

Strict Mode

Enable strict TypeScript options to catch more potential issues

// Generic DataFlow table component
interface TableProps<T> {
  data: T[];
  columns: Array<{
    key: keyof T;
    label: string;
    render?: (value: T[keyof T], row: T) => React.ReactNode;
  }>;
  onRowClick?: (row: T) => void;
}

function DataTable<T>({ data, columns, onRowClick }: TableProps<T>) {
  return (
    <table>
      <thead>
        <tr>
          {columns.map(col => (
            <th key={String(col.key)}>{col.label}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {data.map((row, index) => (
          <tr key={index} onClick={() => onRowClick?.(row)}>
            {columns.map(col => (
              <td key={String(col.key)}>
                {col.render ? col.render(row[col.key], row) : String(row[col.key])}
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}
DataFlow Dashboard

What just happened?

The generic <T> makes this table work with any data type. keyof T ensures column keys exist on the data objects. Custom render functions get full type safety for their parameters.

Common TypeScript Errors

TypeScript errors can be cryptic. Understanding common patterns helps debug issues faster. Most errors relate to missing properties, wrong types, or strict null checks.

Property does not exist on type

Usually means you're accessing a property that TypeScript doesn't know about. Check your interface definitions and make sure all required properties are included.

// Common fixes for TypeScript errors in DataFlow
interface ApiResponse<T> {
  data: T;
  success: boolean;
  message?: string; // Optional property
}

// Type assertion when you know better than TypeScript
const userDiv = document.getElementById('user') as HTMLDivElement;

// Optional chaining prevents crashes
const userName = user?.profile?.displayName ?? 'Guest';

// Non-null assertion when you're certain
const apiKey = process.env.REACT_APP_API_KEY!;

// Union types for multiple possibilities
type LoadingState = 'idle' | 'loading' | 'success' | 'error';
TypeScript strict mode catches more issues but requires explicit handling of null and undefined. Enable it gradually in existing projects to avoid overwhelming error counts.

Best Practices

Start with loose types and tighten gradually. Perfect types aren't required from day one. Focus on public APIs first - component props and function parameters that other developers use. Use any sparingly. When unavoidable, add a comment explaining why and plan to replace it. unknown is safer than any for truly dynamic content. Type your API responses. Create interfaces matching your backend data structures. This catches API changes during development instead of production crashes.

Pro Tip: Gradual Adoption

Rename existing .js files to .tsx one at a time. Fix type errors as you go. Start with leaf components that don't have children, then work up the component tree.

The DataFlow dashboard became much more reliable after adding TypeScript. API changes that previously caused silent failures now generate build errors. Component props are self-documenting through their type definitions. Your IDE becomes significantly more helpful with TypeScript. Autocomplete knows exact property names. Refactoring safely renames properties across the entire codebase. Import statements get generated automatically.

Quiz

1. How do you define props for a DataFlow StatsCard component that requires a title (string) and value (number)?


2. What TypeScript type should you use for a DataFlow search input onChange handler?


3. How do you initialize useState for an array of User objects in the DataFlow user management system?


Up Next: React Accessibility

Make your React applications usable by everyone with proper ARIA attributes, keyboard navigation, and screen reader support.