React TypeScript — Using TypeScript with React (Complete Guide)
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 toimport Reactin every file."strict": true— enables all strict checks. Non-negotiable for production."paths"with@/*— lets you import likeimport { Button } from "@/components/Button"instead of relative nightmare paths."moduleResolution": "bundler"— required for Vite and modern bundlers.
Common Errors with TypeScript + React
Practice Questions
What is the difference between
interfaceandtypein TypeScript? Interfaces support declaration merging and are extendable withextends. Types support unions and mapped types. Use interfaces for object shapes, types for unions and utilities.How do you type an optional prop in a React component? Add
?after the prop name in the interface:age?: number. The prop becomesnumber | undefined.Why would you explicitly pass a generic to
useState<User | null>(null)? When the initial value isnullbut the state will later hold aUser. Without the generic, TypeScript infersnulland prevents assigning aUser.What does
jsx: "react-jsx"in tsconfig do? Enables the automatic JSX runtime (React 17+), removing the need toimport Reactin every.tsxfile.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
- Previous: React Tutorial for Beginners, React Hooks Complete Guide
- Next: React Folder Structure — Best Practices, React State Management — Redux, Zustand, Context API Compared
- Related: TypeScript Cheatsheet, Custom Hooks in React — Reusable Logic
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