TypeScript with React

Building type-safe React applications with TypeScript

⚛️ What is TypeScript with React?

TypeScript enhances React development with type safety for components, props, and state. It provides better IDE support, catches errors early, and makes refactoring safer, resulting in more maintainable and robust React applications.


// Type-safe React component
interface Props {
  name: string;
  age: number;
}

const UserCard: React.FC<Props> = ({ name, age }) => {
  return <div>{name} is {age} years old</div>;
};
                                    

Key Benefits

🎯

Type-Safe Props

Define component prop types

interface ButtonProps {
  label: string;
  onClick: () => void;
}
🔒

State Types

Type-safe state management

const [count, setCount] = 
  useState<number>(0);
🎣

Typed Hooks

Type safety for React hooks

useEffect(() => {
  // Type-safe effect
}, [dependency]);
🛠️

Better IntelliSense

Autocomplete for props and methods

// IDE suggests all props
<Button label="Click" />

🔹 Setting Up React with TypeScript

Create a new React TypeScript project:

# Using Create React App
npx create-react-app my-app --template typescript

# Using Vite (faster)
npm create vite@latest my-app -- --template react-ts

# Navigate to project
cd my-app

# Start development server
npm run dev

🔹 Function Components with Props

Define typed props for function components:

// Method 1: Using interface
interface GreetingProps {
  name: string;
  age?: number; // Optional prop
}

const Greeting: React.FC<GreetingProps> = ({ name, age }) => {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      {age && <p>You are {age} years old</p>}
    </div>
  );
};

// Method 2: Inline props type
const Welcome = ({ message }: { message: string }) => {
  return <h2>{message}</h2>;
};

// Usage
<Greeting name="John" age={25} />
<Welcome message="Welcome to React!" />

🔹 useState Hook with Types

Type-safe state management:

import { useState } from 'react';

// Simple types
const Counter = () => {
  const [count, setCount] = useState<number>(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

// Complex types
interface User {
  id: number;
  name: string;
  email: string;
}

const UserProfile = () => {
  const [user, setUser] = useState<User | null>(null);
  
  const loadUser = () => {
    setUser({ id: 1, name: 'John', email: '[email protected]' });
  };
  
  return (
    <div>
      {user ? (
        <div>
          <h2>{user.name}</h2>
          <p>{user.email}</p>
        </div>
      ) : (
        <button onClick={loadUser}>Load User</button>
      )}
    </div>
  );
};

🔹 Event Handlers

Type-safe event handling:

import React, { ChangeEvent, FormEvent } from 'react';

const LoginForm = () => {
  const [email, setEmail] = React.useState<string>('');
  const [password, setPassword] = React.useState<string>('');

  // Input change handler
  const handleEmailChange = (e: ChangeEvent<HTMLInputElement>) => {
    setEmail(e.target.value);
  };

  // Form submit handler
  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    console.log('Login:', { email, password });
  };

  // Button click handler
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    console.log('Button clicked');
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={handleEmailChange}
        placeholder="Email"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
      />
      <button type="submit">Login</button>
    </form>
  );
};

🔹 useEffect Hook

Type-safe side effects:

import { useEffect, useState } from 'react';

interface Post {
  id: number;
  title: string;
  body: string;
}

const PostList = () => {
  const [posts, setPosts] = useState<Post[]>([]);
  const [loading, setLoading] = useState<boolean>(true);

  useEffect(() => {
    const fetchPosts = async () => {
      try {
        const response = await fetch('https://jsonplaceholder.typicode.com/posts');
        const data: Post[] = await response.json();
        setPosts(data.slice(0, 5));
      } catch (error) {
        console.error('Error fetching posts:', error);
      } finally {
        setLoading(false);
      }
    };

    fetchPosts();
  }, []); // Empty dependency array

  if (loading) return <div>Loading...</div>;

  return (
    <div>
      {posts.map((post) => (
        <div key={post.id}>
          <h3>{post.title}</h3>
          <p>{post.body}</p>
        </div>
      ))}
    </div>
  );
};

🔹 useRef Hook

Type-safe refs:

import { useRef, useEffect } from 'react';

const FocusInput = () => {
  // Ref for input element
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    // Focus input on mount
    inputRef.current?.focus();
  }, []);

  const handleClick = () => {
    // Access input value
    if (inputRef.current) {
      console.log('Input value:', inputRef.current.value);
    }
  };

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="Type here" />
      <button onClick={handleClick}>Log Value</button>
    </div>
  );
};

// Ref for storing values
const Timer = () => {
  const countRef = useRef<number>(0);

  const increment = () => {
    countRef.current += 1;
    console.log('Count:', countRef.current);
  };

  return <button onClick={increment}>Increment</button>;
};

🔹 useContext Hook

Type-safe context:

import { createContext, useContext, useState, ReactNode } from 'react';

// Define context type
interface ThemeContextType {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

// Create context with default value
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

// Provider component
interface ThemeProviderProps {
  children: ReactNode;
}

export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

  const toggleTheme = () => {
    setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

// Custom hook to use context
export const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
};

// Usage in component
const ThemedButton = () => {
  const { theme, toggleTheme } = useTheme();
  
  return (
    <button 
      onClick={toggleTheme}
      style={{ background: theme === 'light' ? '#fff' : '#333' }}
    >
      Current theme: {theme}
    </button>
  );
};

🔹 Custom Hooks

Create reusable type-safe hooks:

import { useState, useEffect } from 'react';

// Custom hook for fetching data
function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        const json = await response.json();
        setData(json);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'An error occurred');
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading, error };
}

// Usage
interface User {
  id: number;
  name: string;
}

const UserList = () => {
  const { data, loading, error } = useFetch<User[]>('/api/users');

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <ul>
      {data?.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
};

🔹 Component Props with Children

Type children prop correctly:

import { ReactNode } from 'react';

// Method 1: Using ReactNode
interface CardProps {
  title: string;
  children: ReactNode;
}

const Card: React.FC<CardProps> = ({ title, children }) => {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div>{children}</div>
    </div>
  );
};

// Method 2: Using PropsWithChildren
import { PropsWithChildren } from 'react';

interface ContainerProps {
  className?: string;
}

const Container = ({ className, children }: PropsWithChildren<ContainerProps>) => {
  return <div className={className}>{children}</div>;
};

// Usage
<Card title="My Card">
  <p>This is the card content</p>
</Card>

🔹 Complete Example: Todo App

Full TypeScript React application:

import { useState } from 'react';

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

const TodoApp = () => {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [input, setInput] = useState<string>('');

  const addTodo = () => {
    if (input.trim()) {
      const newTodo: Todo = {
        id: Date.now(),
        text: input,
        completed: false,
      };
      setTodos([...todos, newTodo]);
      setInput('');
    }
  };

  const toggleTodo = (id: number) => {
    setTodos(
      todos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  const deleteTodo = (id: number) => {
    setTodos(todos.filter((todo) => todo.id !== id));
  };

  return (
    <div>
      <h1>Todo List</h1>
      <div>
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && addTodo()}
          placeholder="Add a todo"
        />
        <button onClick={addTodo}>Add</button>
      </div>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span
              style={{
                textDecoration: todo.completed ? 'line-through' : 'none',
              }}
            >
              {todo.text}
            </span>
            <button onClick={() => deleteTodo(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default TodoApp;

🧠 Test Your Knowledge

How do you define optional props in TypeScript React?