TypeScript with React
TypeScript and React work together to build component-based web applications with full type safety. React components accept inputs called props and manage internal data called state. TypeScript adds types to both, catching bugs before the browser even runs the code — like a misspelled prop name or passing a number where a string is expected.
Setting Up a React TypeScript Project
# Create a new project with TypeScript npx create-react-app my-app --template typescript # Or with Vite (faster) npm create vite@latest my-app -- --template react-ts Project files created: src/ ├── App.tsx (TypeScript + JSX = .tsx extension) ├── main.tsx └── index.css
Function Components with Types
Every React component written in TypeScript uses the .tsx file extension. The component is a function that receives props and returns JSX.
// Greeting.tsx
interface GreetingProps {
name: string;
role?: string; // optional prop
}
function Greeting({ name, role }: GreetingProps) {
return (
<div>
<h2>Hello, {name}!</h2>
{role && <p>Role: {role}</p>}
</div>
);
}
export default Greeting;
// Using the component
<Greeting name="Anita" /> // OK — role is optional
<Greeting name="Anita" role="Admin" /> // OK
<Greeting /> // Error: 'name' is required
<Greeting name={42} /> // Error: number is not string
React.FC Type (Alternative)
import React from "react";
interface BadgeProps {
label: string;
color: "red" | "green" | "blue";
}
const Badge: React.FC<BadgeProps> = ({ label, color }) => {
return <span style={{ color }}>{label}</span>;
};
export default Badge;
useState with Types
useState accepts a generic type parameter that defines what the state variable can hold.
import { useState } from "react";
interface Task {
id: number;
title: string;
done: boolean;
}
function TaskList() {
let [tasks, setTasks] = useState<Task[]>([]);
let [input, setInput] = useState<string>("");
let [count, setCount] = useState<number>(0);
let [error, setError] = useState<string | null>(null);
function addTask() {
if (input.trim() === "") {
setError("Task title cannot be empty");
return;
}
let newTask: Task = {
id: count + 1,
title: input,
done: false
};
setTasks([...tasks, newTask]);
setCount(count + 1);
setInput("");
setError(null);
}
function toggleTask(id: number) {
setTasks(tasks.map(t =>
t.id === id ? { ...t, done: !t.done } : t
));
}
return (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Add a task"
/>
<button onClick={addTask}>Add</button>
{error && <p style={{ color: "red" }}>{error}</p>}
<ul>
{tasks.map(task => (
<li
key={task.id}
onClick={() => toggleTask(task.id)}
style={{ textDecoration: task.done ? "line-through" : "none" }}
>
{task.title}
</li>
))}
</ul>
</div>
);
}
useEffect with Types
import { useState, useEffect } from "react";
interface Post {
id: number;
title: string;
body: string;
}
function PostViewer({ postId }: { postId: number }) {
let [post, setPost] = useState<Post | null>(null);
let [loading, setLoading] = useState<boolean>(true);
let [error, setError] = useState<string | null>(null);
useEffect(() => {
setLoading(true);
setError(null);
fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`)
.then(res => {
if (!res.ok) throw new Error("Post not found");
return res.json();
})
.then((data: Post) => {
setPost(data);
setLoading(false);
})
.catch((err: Error) => {
setError(err.message);
setLoading(false);
});
}, [postId]);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
if (!post) return null;
return (
<div>
<h3>{post.title}</h3>
<p>{post.body}</p>
</div>
);
}
Event Handling with Types
React event types match the HTML element they come from:
<input onChange> → React.ChangeEvent<HTMLInputElement>
<form onSubmit> → React.FormEvent<HTMLFormElement>
<button onClick> → React.MouseEvent<HTMLButtonElement>
<div onKeyDown> → React.KeyboardEvent<HTMLDivElement>
function SearchBar() {
let [query, setQuery] = useState<string>("");
function handleChange(e: React.ChangeEvent<HTMLInputElement>): void {
setQuery(e.target.value);
}
function handleSubmit(e: React.FormEvent<HTMLFormElement>): void {
e.preventDefault();
console.log("Searching for:", query);
}
return (
<form onSubmit={handleSubmit}>
<input value={query} onChange={handleChange} />
<button type="submit">Search</button>
</form>
);
}
useRef with Types
import { useRef } from "react";
function AutoFocusInput() {
// Ref typed as HTMLInputElement | null
let inputRef = useRef<HTMLInputElement>(null);
function focusInput(): void {
inputRef.current?.focus(); // optional chaining — current might be null
}
return (
<div>
<input ref={inputRef} placeholder="Type here" />
<button onClick={focusInput}>Focus Input</button>
</div>
);
}
Props with Children
interface CardProps {
title: string;
children: React.ReactNode; // accepts any valid JSX as children
}
function Card({ title, children }: CardProps) {
return (
<div className="card">
<h3>{title}</h3>
<div>{children}</div>
</div>
);
}
// Usage
<Card title="Profile">
<p>Name: Shruti</p>
<p>Age: 24</p>
</Card>
Component Props Summary
| Pattern | TypeScript Type | Use Case |
|---|---|---|
| Required prop | name: string | Must always be passed |
| Optional prop | role?: string | May or may not be passed |
| Literal union prop | size: "sm" | "md" | "lg" | Only specific values allowed |
| Callback prop | onClick: () => void | Parent passes a function to child |
| Children prop | children: React.ReactNode | Wrap other components inside |
| State type | useState<string>("") | Typed internal component data |
