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

PatternTypeScript TypeUse Case
Required propname: stringMust always be passed
Optional proprole?: stringMay or may not be passed
Literal union propsize: "sm" | "md" | "lg"Only specific values allowed
Callback proponClick: () => voidParent passes a function to child
Children propchildren: React.ReactNodeWrap other components inside
State typeuseState<string>("")Typed internal component data

Leave a Comment

Your email address will not be published. Required fields are marked *