Skip to content

React TypeScript — Using TypeScript with React (Complete Guide)

DodaTech 9 min read

In this tutorial, you'll learn about React TypeScript. We cover key concepts, practical examples, and best practices.

React TypeScript combines static types with React's component model to catch prop, state, and event handler bugs at compile time instead of runtime.

What You'll Learn

Add static types to React components — prop interfaces, useState generics, event handlers, custom hooks, and configuring tsconfig for JSX support.

Why It Matters

TypeScript catches entire categories of bugs before they reach the browser. In a team of five or more developers, untyped React props cause countless "undefined is not a function" errors. TypeScript prevents them.

Real-World Use

Building a data dashboard where every prop type is enforced — a UserProfile component that only accepts valid user shapes, a form handler that knows the exact event types, and reusable hooks with typed return values used by Durga Antivirus Pro's frontend for real-time threat display.

Setting Up TypeScript with React

The quickest way is using Vite, which scaffolds TypeScript support automatically:

npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev

Expected output:

VITE v5.x  ready in 300ms
➜  Local:   http://localhost:5173/

Notice the --template react-ts flag. This generates .tsx files (TypeScript + JSX) and a tsconfig.json. You can also use Create React App with npx create-react-app my-app --template <a href="/programming-languages/typescript/">typescript</a>.

Typing Props with Interfaces

Props are the main way data flows between components. Without types, a component accepts anything — and breaks silently. With TypeScript, you define exactly what shape the props must have:

interface GreetingProps {
  name: string;
  age?: number;        // optional prop
  role: "admin" | "user" | "guest";  // union literal
  onLogout: () => void;               // callback
}

function Greeting({ name, age, role, onLogout }: GreetingProps) {
  return (
    <div style={{ padding: "1rem", border: "1px solid #ccc" }}>
      <h1>Hello, {name}!</h1>
      {age && <p>Age: {age}</p>}
      <p>Role: <strong>{role}</strong></p>
      <button onClick={onLogout}>Logout</button>
    </div>
  );
}

Why this helps: If someone passes role="superadmin", TypeScript immediately shows a red underline. The error is caught during development, not in production.

Interface vs Type — When to Use Which

A common question: should you use interface or type?

// interface — extendable, preferred for object shapes
interface User {
  id: number;
  name: string;
  email: string;
}

interface AdminUser extends User {
  permissions: string[];
}

// type — prefer for unions, intersections, primitives
type Status = "loading" | "success" | "error";
type ApiResponse<T> = {
  data: T | null;
  error: string | null;
  loading: boolean;
};
Feature interface type
Extend/merge ✅ extends ✅ intersections (&)
Declaration merging ✅ Yes ❌ No
Union types ❌ No ✅ Yes
Mapped types ❌ No ✅ Yes
Performance Slightly faster Slightly slower

Rule of thumb: Use interface for public API shapes (props, state objects). Use type for everything else — unions, intersections, and utility types.

useState with Generics

The useState hook infers the type from the initial value, but sometimes you need to be explicit — especially when the state starts as null:

import { useState } from "react";

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

// TypeScript infers: User | null
const [user, setUser] = useState<User | null>(null);

// Later in a fetch:
setUser({ id: 1, name: "Alice", email: "alice@example.com" });

// This would be an error — missing 'email'
// setUser({ id: 1, name: "Alice" });  // ❌

For complex initial state, use a lazy initializer:

const [config, setConfig] = useState<Config>(() => {
  const saved = localStorage.getItem("app-config");
  return saved ? JSON.parse(saved) : defaultConfig;
});

Event Handling with Proper Types

React's synthetic events have their own types. The two you'll use most:

import { useState, ChangeEvent, FormEvent } from "react";

function LoginForm() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  function handleChange(e: ChangeEvent<HTMLInputElement>) {
    const { name, value } = e.target;
    if (name === "email") setEmail(value);
    if (name === "password") setPassword(value);
  }

  function handleSubmit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    console.log("Logging in with:", { email, password });
  }

  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: 400, margin: "2rem auto" }}>
      <input name="email" value={email} onChange={handleChange} placeholder="Email" />
      <input name="password" type="password" value={password} onChange={handleChange} placeholder="Password" />
      <button type="submit">Login</button>
    </form>
  );
}

Expected output (after submit): Logging in with: { email: "alice@example.com", password: "secret123" }

Common event types:

Event Type
Click MouseEvent<HTMLButtonElement>
Change (input) ChangeEvent<HTMLInputElement>
Submit (form) FormEvent<HTMLFormElement>
Key press KeyboardEvent<HTMLInputElement>
Focus FocusEvent<HTMLInputElement>

Typing Custom Hooks

When you build React custom hooks, TypeScript ensures the return type is predictable for every consumer:

import { useState, useEffect } from "react";

interface FetchResult<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
}

function useFetch<T>(url: string): FetchResult<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let cancelled = false;
    async function fetchData() {
      try {
        setLoading(true);
        const res = await fetch(url);
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const json = await res.json();
        if (!cancelled) setData(json);
      } catch (err) {
        if (!cancelled) setError((err as Error).message);
      } finally {
        if (!cancelled) setLoading(false);
      }
    }
    fetchData();
    return () => { cancelled = true; };
  }, [url]);

  return { data, loading, error };
}

// Usage — the generic infers the return type!
interface Product {
  id: number;
  title: string;
  price: number;
}

function ProductList() {
  const { data: products, loading, error } = useFetch<Product[]>("/api/products");

  if (loading) return <p>Loading...</p>;
  if (error) return <p style={{ color: "red" }}>Error: {error}</p>;

  return (
    <ul>
      {products?.map(p => (
        <li key={p.id}>{p.title} — ${p.price}</li>
      ))}
    </ul>
  );
}

The generic <T> lets useFetch work with any data shape. Each call site gets full autocomplete and type checking.

tsconfig.json for React

Here is a production-ready tsconfig.json for React projects:

{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "allowImportingTsExtensions": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

Key settings explained:

  • "jsx": "react-jsx" — enables the new JSX transform (React 17+). No need to import React in every file.
  • "strict": true — enables all strict checks. Non-negotiable for production.
  • "paths" with @/* — lets you import like import { Button } from "@/components/Button" instead of relative nightmare paths.
  • "moduleResolution": "bundler" — required for Vite and modern bundlers.

Common Errors with TypeScript + React

Error: 'X' is not assignable to type 'IntrinsicAttributes'

This means you passed a prop to a component that doesn't exist in its props interface. Example: <Button variant="primary"> but variant isn't defined in ButtonProps. Check the interface definition.

Error: Object is possibly 'null'

TypeScript flags values that might be null. Fix with early returns, optional chaining (?.), or narrowing: if (user === null) return <p>Loading</p>;.

Error: 'children' is not defined in Props

You forgot to add children to your interface. Add children: React.ReactNode — it accepts anything React can render (strings, JSX, arrays, fragments, etc.).

Error: Cannot find module './styles.css' or its corresponding type declarations

CSS modules need type declarations. Create a src/declarations.d.ts file: declare module "*.module.css" { const classes: { readonly [key: string]: string }; export default classes; }.

Error: Type 'undefined' is not assignable to type 'string'

A prop or state value might be undefined. Make the type include undefined: name?: string (optional) or provide a default value. Enable strictNullChecks in tsconfig to catch these early

Error: 'X' is used before being assigned

TypeScript detected a variable used before a value was guaranteed. Use definite assignment: let name!: string; or initialize it: let name = "";.

Practice Questions

  1. What is the difference between interface and type in TypeScript? Interfaces support declaration merging and are extendable with extends. Types support unions and mapped types. Use interfaces for object shapes, types for unions and utilities.

  2. How do you type an optional prop in a React component? Add ? after the prop name in the interface: age?: number. The prop becomes number | undefined.

  3. Why would you explicitly pass a generic to useState<User | null>(null)? When the initial value is null but the state will later hold a User. Without the generic, TypeScript infers null and prevents assigning a User.

  4. What does jsx: "react-jsx" in tsconfig do? Enables the automatic JSX runtime (React 17+), removing the need to import React in every .tsx file.

  5. How do you type the event parameter in a form submit handler? Use FormEvent<HTMLFormElement>. The generic parameter specifies which DOM element the event belongs to.

Challenge

Build a typed useForm hook that accepts a generic type for form values and returns typed values, errors, handleChange, and handleSubmit. The hook should enforce that every field in the generic type has a corresponding error string or null. For example: useForm<{ email: string; password: string }>() should infer values.email as string and errors.email as string | null.

Real-World Task

Take a React component from your current project that has no TypeScript types. Add a full props interface, type all event handlers, and add proper generic types to any hooks it uses. Verify with npx tsc --noEmit that zero errors remain.

Mini Project: Typed Dashboard Widget

Build a DashboardWidget component that accepts a generic data type and renders a configurable widget with title, data, and an optional footer:

interface WidgetProps<T> {
  title: string;
  data: T;
  renderItem: (item: T) => React.ReactNode;
  footer?: React.ReactNode;
}

function DashboardWidget<T>({ title, data, renderItem, footer }: WidgetProps<T>) {
  return (
    <div style={{ border: "1px solid #ddd", borderRadius: 8, padding: 16 }}>
      <h2>{title}</h2>
      <div>{renderItem(data)}</div>
      {footer && <div style={{ marginTop: 12, borderTop: "1px solid #eee", paddingTop: 8 }}>{footer}</div>}
    </div>
  );
}

Learning Path

flowchart LR
  A[React Tutorial for Beginners] --> B[React Hooks Complete Guide]
  B --> C[React TypeScript]
  C --> D[React Folder Structure]
  D --> E[React State Management]
  C -.-> F[Custom Hooks in React]
  style C fill:#4a90d9,color:#fff

Next Steps

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. This pattern is used in Durga Antivirus Pro's frontend to type-safe threat data from the scanning engine — ensuring every alert, signature match, and status update conforms to the expected schema before it reaches the UI.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro