DailyDevDiet

logo - dailydevdiet

Learn. Build. Innovate. Elevate your coding skills with dailydevdiet!

Chapter 30: React with TypeScript

React with TypeScript

Introduction

TypeScript is a powerful superset of JavaScript that adds static type checking to your code. When combined with React, it provides enhanced developer experience, better code quality, and improved maintainability. This chapter will guide you through integrating TypeScript with React applications.

What is TypeScript?

TypeScript is a programming language developed by Microsoft that builds on JavaScript by adding static type definitions. It compiles to plain JavaScript and runs anywhere JavaScript runs.

Benefits of Using TypeScript with React

  1. Type Safety: Catch errors at compile time rather than runtime
  2. Better IDE Support: Enhanced autocomplete, refactoring, and navigation
  3. Self-Documenting Code: Types serve as documentation
  4. Improved Refactoring: Safer code changes with confidence
  5. Better Team Collaboration: Clear interfaces and contracts

Setting Up React with TypeScript

Creating a New React TypeScript Project

# Using Create React App with TypeScript template
npx create-react-app my-app --template typescript
# Using Vite
npm create vite@latest my-react-app -- --template react-ts
# Using Next.js with TypeScript
npx create-next-app@latest my-app --typescript

Adding TypeScript to Existing React Project

# Install TypeScript and type definitions
npm install --save-dev typescript @types/node @types/react @types/react-dom
# Create tsconfig.json
npx tsc --init

Basic tsconfig.json for React

{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "es6"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": [
    "src"
  ]
}

TypeScript Basics for React

Basic Types

// Primitive types
let name: string = "John";
let age: number = 25;
let isActive: boolean = true;
let data: null = null;
let value: undefined = undefined;
// Array types
let numbers: number[] = [1, 2, 3];
let names: Array<string> = ["Alice", "Bob"];
// Object types
let user: {
  name: string;
  age: number;
  email?: string; // Optional property
} = {
  name: "John",
  age: 25
};
// Union types
let id: string | number = "123";

// Literal types
let status: "loading" | "success" | "error" = "loading";

Interfaces and Types

// Interface definition
interface User {
  id: number;
  name: string;
  email: string;
  age?: number; // Optional property
  readonly createdAt: Date; // Read-only property
}
// Type alias
type Theme = "light" | "dark";
type ButtonVariant = "primary" | "secondary" | "danger";
// Extending interfaces
interface Admin extends User {
  permissions: string[];
}
// Generic interfaces
interface ApiResponse<T> {
  data: T;
  status: number;
  message: 

Typing React Components

Function Components

import React from 'react';

// Basic function component
const Greeting: React.FC = () => {
  return <h1>Hello, World!</h1>;
};
// Function component with props
interface GreetingProps {
  name: string;
  age?: number;
}

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

Props with Children

interface CardProps {
  title: string;
  children: React.ReactNode;
}

const Card: React.FC<CardProps> = ({ title, children }) => {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-content">
        {children}
      </div>
    </div>
  );
};
// Usage
<Card title="My Card">
  <p>This is the card content</p>
</Card>

Class Components

import React, { Component } from 'react';
interface CounterProps {
  initialValue?: number;
}

interface CounterState {
  count: number;
}

class Counter extends Component<CounterProps, CounterState> {
  constructor(props: CounterProps) {
    super(props);
    this.state = {
      count: props.initialValue || 0
    };
  }

  increment = (): void => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.increment}>Increment</button>
      </div>
    );
  }
}

Typing React Hooks

useState Hook

import React, { useState } from 'react';
const UserProfile: React.FC = () => {
  // TypeScript can infer the type
  const [name, setName] = useState("John");
  // Explicit typing
  const [age, setAge] = useState<number>(25);
 
  // Union types
  const [status, setStatus] = useState<"loading" | "success" | "error">("loading");
  // Complex objects
  interface User {
    id: number;
    name: string;
    email: string;
  }
  const [user, setUser] = useState<User | null>(null);
  return (
    <div>
      <p>Name: {name}</p>
      <p>Age: {age}</p>
      <p>Status: {status}</p>
      <button onClick={() => setName("Jane")}>Change Name</button>
    </div>
  );
};

useEffect Hook

import React, { useState, useEffect } from 'react';
interface Post {
  id: number;
  title: string;
  body: string;
}
const PostList: React.FC = () => {
  const [posts, setPosts] = useState<Post[]>([]);
  const [loading, setLoading] = useState<boolean>(true);
  useEffect(() => {
    const fetchPosts = async (): Promise<void> => {
      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();
  }, []);
  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

import React, { useRef, useEffect } from 'react';
const FocusInput: React.FC = () => {
  const inputRef = useRef<HTMLInputElement>(null);
  const countRef = useRef<number>(0);
  useEffect(() => {
    // Focus the input when component mounts
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }, []);
  const handleClick = (): void => {
    countRef.current += 1;
    console.log(`Clicked ${countRef.current} times`);
  };
  return (
    <div>
      <input ref={inputRef} type="text" placeholder="This will be focused" />
      <button onClick={handleClick}>Click me</button>
    </div>
  );
};

useContext Hook

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

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

// Create context with default value
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
// Theme provider component
const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [theme, setTheme] = useState<"light" | "dark">("light");
  const toggleTheme = (): void => {
    setTheme(prev => prev === "light" ? "dark" : "light");
  };
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};
// Custom hook to use theme context
const useTheme = (): ThemeContextType => {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
};
// Component using the context
const ThemedButton: React.FC = () => {
  const { theme, toggleTheme } = useTheme();
  return (
    <button
      onClick={toggleTheme}
      style={{
        backgroundColor: theme === "light" ? "#fff" : "#333",
        color: theme === "light" ? "#333" : "#fff"
      }}
    >
      Toggle Theme (Current: {theme})
    </button>
  );
};

Event Handling with TypeScript

Common Event Types

import React from 'react';
const EventHandlingExample: React.FC = () => {
  // Button click event
  const handleClick = (event: React.MouseEvent<HTMLButtonElement>): void => {
    console.log('Button clicked!', event.currentTarget);
  };
  // Input change event
  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
    console.log('Input value:', event.target.value);
  };
  // Form submit event
  const handleSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
    event.preventDefault();
    console.log('Form submitted!');
  };
  // Key press event
  const handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>): void => {
    if (event.key === 'Enter') {
      console.log('Enter key pressed!');
    }
  };
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        onChange={handleInputChange}
        onKeyPress={handleKeyPress}
        placeholder="Type something..."
      />
      <button type="submit" onClick={handleClick}>
        Submit
      </button>
    </form>
  );
};

Advanced TypeScript Patterns

Generic Components

import React from 'react'
// Generic list component
interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{renderItem(item, index)}</li>
      ))}
    </ul>
  );
}
// Usage
const App: React.FC = () => {
  const users = [
    { id: 1, name: "John", email: "john@example.com" },
    { id: 2, name: "Jane", email: "jane@example.com" }
  ];
  const numbers = [1, 2, 3, 4, 5];
  return (
    <div>
      <List
        items={users}
        renderItem={(user) => (
          <span>{user.name} - {user.email}</span>
        )}
      />
      <List
        items={numbers}
        renderItem={(number) => <strong>{number * 2}</strong>}
      />
    </div>
  );
};

Higher-Order Components with TypeScript

import React, { Component } from 'react';

// HOC that adds loading state
interface WithLoadingProps {
  loading: boolean;
}

function withLoading<P extends object>(
  WrappedComponent: React.ComponentType<P>
): React.ComponentType<P & WithLoadingProps> {
  return class WithLoadingComponent extends Component<P & WithLoadingProps> {
    render() {
      const { loading, ...otherProps } = this.props;
     
      if (loading) {
        return <div>Loading...</div>;
      }
     
      return <WrappedComponent {...(otherProps as P)} />;
    }
  };
}
// Component to wrap
interface UserListProps {
  users: Array<{ id: number; name: string }>;
}
const UserList: React.FC<UserListProps> = ({ users }) => (
  <ul>
    {users.map(user => (
      <li key={user.id}>{user.name}</li>
    ))}
  </ul>
);
// Enhanced component
const UserListWithLoading = withLoading(UserList);
// Usage
const App: React.FC = () => {
  const users = [{ id: 1, name: "John" }, { id: 2, name: "Jane" }];
    return (
    <UserListWithLoading
      users={users}
      loading={false}
    />
  );
};

Working with APIs and TypeScript

Typed API Responses

import React, { useState, useEffect } from 'react';
// Define API response types
interface User {
  id: number;
  name: string;
  username: string;
  email: string;
  address: {
    street: string;
    city: string;
    zipcode: string;
  };
}

interface ApiError {
  message: string;
  status: number;
}
// Custom hook for API calls
const useApi = <T>(url: string) => {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<ApiError | null>(null);
  useEffect(() => {
    const fetchData = async (): Promise<void> => {
      try {
        setLoading(true);
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const result: T = await response.json();
        setData(result);
      } catch (err) {
        setError({
          message: err instanceof Error ? err.message : 'An error occurred',
          status: 500
        });
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [url]);

  return { data, loading, error };
};
// Component using the custom hook
const UserProfile: React.FC<{ userId: number }> = ({ userId }) => {
  const { data: user, loading, error } = useApi<User>(
    `https://jsonplaceholder.typicode.com/users/${userId}`
  );
  if (loading) return <div>Loading user...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!user) return <div>No user found</div>;
  return (
    <div>
      <h2>{user.name}</h2>
      <p>Username: {user.username}</p>
      <p>Email: {user.email}</p>
      <address>
        {user.address.street}, {user.address.city} {user.address.zipcode}
      </address>
    </div>
  );
};

TypeScript with React Router

import React from 'react';
import { BrowserRouter as Router, Route, Routes, useParams, Link } from 'react-router-dom';
// Define route parameters
interface UserParams {
  id: string;
}
const UserDetail: React.FC = () => {
  const { id } = useParams<UserParams>();
  return (
    <div>
      <h2>User ID: {id}</h2>
      <Link to="/">Back to Home</Link>
    </div>
  );
};
const Home: React.FC = () => {
  return (
    <div>
      <h1>Home Page</h1>
      <Link to="/user/123">Go to User 123</Link>
    </div>
  );
};

const App: React.FC = () => {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/user/:id" element={<UserDetail />} />
      </Routes>
    </Router>
  );
};

Common TypeScript Patterns and Best Practices

1. Prop Types and Default Props

interface ButtonProps {
  children: React.ReactNode;
  variant?: "primary" | "secondary" | "danger";
  size?: "small" | "medium" | "large";
  disabled?: boolean;
  onClick?: () => void;
}
const Button: React.FC<ButtonProps> = ({
  children,
  variant = "primary",
  size = "medium",
  disabled = false,
  onClick
}) => {
  return (
    <button
      className={`btn btn-${variant} btn-${size}`}
      disabled={disabled}
      onClick={onClick}
    >
      {children}
    </button>
  );
};

2. Utility Types

// Pick specific properties
interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}
type UserSummary = Pick<User, "id" | "name">;

// Omit specific properties
type CreateUser = Omit<User, "id">;
// Partial (all properties optional)
type UpdateUser = Partial<User>;
// Required (all properties required)
type RequiredUser = Required<User>;

3. Discriminated Unions

interface LoadingState {
  status: "loading";
}
interface SuccessState {
  status: "success";
  data: any;
}
interface ErrorState {
  status: "error";
  error: string;
}
type AsyncState = LoadingState | SuccessState | ErrorState;
const AsyncComponent: React.FC<{ state: AsyncState }> = ({ state }) => {
  switch (state.status) {
    case "loading":
      return <div>Loading...</div>;
    case "success":
      return <div>Data: {JSON.stringify(state.data)}</div>;
    case "error":
      return <div>Error: {state.error}</div>;
    default:
      // TypeScript ensures this case is never reached
      const exhaustiveCheck: never = state;
      return exhaustiveCheck;
  }
};

TypeScript Configuration for React

Strict Mode Configuration

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  }
}

Path Mapping

{
  "compilerOptions": {
    "baseUrl": "src",
    "paths": {
      "@components/*": ["components/*"],
      "@utils/*": ["utils/*"],
      "@types/*": ["types/*"]
    }
  }
}

Common TypeScript Errors and Solutions

1. Property Does Not Exist on Type

// Error: Property 'value' does not exist on type 'EventTarget'
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  console.log(event.target.value); // ✅ Correct
  // console.log(event.target.value); // ❌ Error if using Event instead
};

2. Type Assertion

/// Sometimes you need to tell TypeScript about types
const element = document.getElementById('my-element') as HTMLInputElement;
element.value = 'Hello'; // Now TypeScript knows it's an input element

3. Optional Chaining and Nullish Coalescing

interface User {
  name: string;
  profile?: {
    avatar?: string;
  };
}
const UserAvatar: React.FC<{ user: User }> = ({ user }) => {
  // Optional chaining
  const avatarUrl = user.profile?.avatar;
  // Nullish coalescing
  const displayAvatar = avatarUrl ?? '/default-avatar.png';
  return <img src={displayAvatar} alt="User avatar" />;
};

Testing TypeScript React Components

import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
interface CounterProps {
  initialValue?: number;
}
const Counter: React.FC<CounterProps> = ({ initialValue = 0 }) => {
  const [count, setCount] = React.useState(initialValue);
  return (
    <div>
      <span data-testid="count">{count}</span>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};
// Test file
describe('Counter', () => {
  test('renders with initial value', () => {
    render(<Counter initialValue={5} />);
    expect(screen.getByTestId('count')).toHaveTextContent('5');
  });
  test('increments count when button is clicked', () => {
    render(<Counter />);
    const button = screen.getByRole('button', { name: /increment/i });
    fireEvent.click(button);
    expect(screen.getByTestId('count')).toHaveTextContent('1');
  });
});

Migration Strategies

Gradual Migration from JavaScript

  1. Start with .tsx files: Rename .js files to .tsx
  2. Add basic types: Start with props and state
  3. Enable strict mode gradually: Add strict checks one by one
  4. Type external libraries: Add type definitions for third-party packages

Converting a JavaScript Component

// Before (JavaScript)
const UserCard = ({ user, onEdit }) => {
  return (
    <div className="user-card">
      <h3>{user.name}</h3>
      <p>{user.email}</p>
      <button onClick={() => onEdit(user.id)}>Edit</button>
    </div>
  );
};
// After (TypeScript)
interface User {
  id: number;
  name: string;
  email: string;
}
interface UserCardProps {
  user: User;
  onEdit: (userId: number) => void;
}
const UserCard: React.FC<UserCardProps> = ({ user, onEdit }) => {
  return (
    <div className="user-card">
      <h3>{user.name}</h3>
      <p>{user.email}</p>
      <button onClick={() => onEdit(user.id)}>Edit</button>
    </div>
  );
};

Performance Considerations

React.memo with TypeScript

interface ExpensiveComponentProps {
  data: string[];
  processingFunction: (data: string[]) => string[];
}
const ExpensiveComponent = React.memo<ExpensiveComponentProps>(({ data, processingFunction }) => {
  const processedData = React.useMemo(() => {
    return processingFunction(data);
  }, [data, processingFunction]);
  return (
    <div>
      {processedData.map((item, index) => (
        <div key={index}>{item}</div>
      ))}
    </div>
  );
});

Conclusion

TypeScript significantly enhances the React development experience by providing:

  1. Type Safety: Catch errors early in development
  2. Better Developer Experience: Enhanced IDE support and autocompletion
  3. Improved Code Quality: Self-documenting code with clear interfaces
  4. Easier Refactoring: Confident code changes with type checking
  5. Better Team Collaboration: Clear contracts between components

Key Takeaways

  • Start with basic typing and gradually add more complex types
  • Use interfaces for object shapes and props
  • Leverage TypeScript’s utility types for common patterns
  • Take advantage of type inference where possible
  • Use strict mode for better type safety
  • Consider gradual migration for existing projects

Next Steps

  • Practice typing various React patterns
  • Explore advanced TypeScript features like mapped types and conditional types
  • Learn about type-only imports and exports
  • Study real-world TypeScript React projects
  • Set up proper linting and tooling for TypeScript projects

TypeScript and React work beautifully together, creating a powerful development environment that scales well for large applications while maintaining excellent developer experience.

Related Articles

Scroll to Top