TypeScript in React: Best Practices

By Nguyen Hoang Diep Phi
TypeScriptReactJavaScriptBest Practices
TypeScript

TypeScript brings type safety to React applications. Let's explore how to use TypeScript effectively in React projects.

Defining Props Interface

interface ButtonProps {
  text: string;
  onClick: () => void;
  variant?: "primary" | "secondary";
  disabled?: boolean;
}

const Button: React.FC<ButtonProps> = ({
  text,
  onClick,
  variant = "primary",
  disabled = false,
}) => {
  return (
    <button
      className={`btn btn-${variant}`}
      onClick={onClick}
      disabled={disabled}
    >
      {text}
    </button>
  );
};

State Management with TypeScript

useState with Types

import React, { useState } from "react";

interface User {
  id: number;
  name: string;
  email: string;
}

const UserProfile: React.FC = () => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState<boolean>(false);

  const fetchUser = async (id: number) => {
    setLoading(true);
    try {
      const response = await fetch(`/api/users/${id}`);
      const userData: User = await response.json();
      setUser(userData);
    } catch (error) {
      console.error("Error fetching user:", error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div>
      {loading ? (
        <p>Loading...</p>
      ) : user ? (
        <div>
          <h1>{user.name}</h1>
          <p>{user.email}</p>
        </div>
      ) : (
        <p>No user data</p>
      )}
    </div>
  );
};

useReducer with TypeScript

import React, { useReducer } from "react";

interface TodoState {
  todos: Todo[];
  filter: "all" | "completed" | "active";
}

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

type TodoAction =
  | { type: "ADD_TODO"; payload: string }
  | { type: "TOGGLE_TODO"; payload: number }
  | { type: "SET_FILTER"; payload: TodoState["filter"] };

const todoReducer = (state: TodoState, action: TodoAction): TodoState => {
  switch (action.type) {
    case "ADD_TODO":
      return {
        ...state,
        todos: [
          ...state.todos,
          { id: Date.now(), text: action.payload, completed: false },
        ],
      };
    case "TOGGLE_TODO":
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.payload
            ? { ...todo, completed: !todo.completed }
            : todo,
        ),
      };
    case "SET_FILTER":
      return {
        ...state,
        filter: action.payload,
      };
    default:
      return state;
  }
};

Event Handling

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

const ContactForm: React.FC = () => {
  const [formData, setFormData] = useState({
    name: "",
    email: "",
    message: "",
  });

  const handleInputChange = (
    e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
  ) => {
    const { name, value } = e.target;
    setFormData((prev) => ({
      ...prev,
      [name]: value,
    }));
  };

  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    console.log("Form submitted:", formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        name="name"
        value={formData.name}
        onChange={handleInputChange}
        placeholder="Your name"
      />
      <input
        type="email"
        name="email"
        value={formData.email}
        onChange={handleInputChange}
        placeholder="Your email"
      />
      <textarea
        name="message"
        value={formData.message}
        onChange={handleInputChange}
        placeholder="Your message"
      />
      <button type="submit">Send</button>
    </form>
  );
};

Generic Components

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string | number;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li key={keyExtractor(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// Usage
const users: User[] = [
  { id: 1, name: "John", email: "john@example.com" },
  { id: 2, name: "Jane", email: "jane@example.com" },
];

<List
  items={users}
  renderItem={(user) => (
    <span>
      {user.name} ({user.email})
    </span>
  )}
  keyExtractor={(user) => user.id}
/>;

Custom Hooks with TypeScript

import { useState, useEffect } from "react";

interface FetchState<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
}

function useFetch<T>(url: string): FetchState<T> {
  const [state, setState] = useState<FetchState<T>>({
    data: null,
    loading: true,
    error: null,
  });

  useEffect(() => {
    const fetchData = async () => {
      try {
        setState((prev) => ({ ...prev, loading: true }));
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const data: T = await response.json();
        setState({ data, loading: false, error: null });
      } catch (error) {
        setState({
          data: null,
          loading: false,
          error: error instanceof Error ? error.message : "An error occurred",
        });
      }
    };

    fetchData();
  }, [url]);

  return state;
}

Best Practices

1. Use Strict TypeScript Configuration

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  }
}

2. Prefer Type over Interface for Unions

// Good for unions
type Status = "loading" | "success" | "error";

// Good for object shapes
interface User {
  id: number;
  name: string;
  email: string;
}

3. Use Utility Types

// Partial for optional properties
const updateUser = (id: number, updates: Partial<User>) => {
  // Implementation
};

// Pick for selecting specific properties
type UserPreview = Pick<User, "id" | "name">;

// Omit for excluding properties
type CreateUserRequest = Omit<User, "id">;

Conclusion

TypeScript significantly improves the React development experience by providing:

  • Compile-time error detection
  • Better IDE support with autocomplete
  • Self-documenting code through types
  • Easier refactoring and maintenance

By following these best practices, you'll write more reliable and maintainable React applications with TypeScript!

© 2026. All rights reserved.