IA
Indra Arianggi
Back to Blog

TypeScript Tips for React Developers

5 min read
TypeScript React Best Practices Development
TypeScript Tips for React Developers

Introduction

TypeScript has become an essential tool in modern React development, providing type safety and improved developer experience. This guide covers essential TypeScript patterns and best practices specifically for React applications.

Type-Safe Props

Basic Props Interface

Start with properly typing your component props:

interface UserCardProps {
  name: string;
  email: string;
  age?: number; // Optional prop
  onProfileClick: (userId: string) => void;
}
 
const UserCard: React.FC<UserCardProps> = ({
  name,
  email,
  age,
  onProfileClick,
}) => {
  return (
    <div onClick={() => onProfileClick(name)}>
      <h2>{name}</h2>
      <p>{email}</p>
      {age && <p>Age: {age}</p>}
    </div>
  );
};

Generic Components

Create reusable components with generics:

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}
 
function List<T>({ items, renderItem }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

State Management with TypeScript

useState with Type Inference

interface User {
  id: string;
  name: string;
  email: string;
}
 
const [user, setUser] = useState<User | null>(null);

useReducer with Discriminated Unions

type Action =
  | { type: "SET_LOADING" }
  | { type: "SET_DATA"; payload: string[] }
  | { type: "SET_ERROR"; payload: string };
 
interface State {
  loading: boolean;
  data: string[] | null;
  error: string | null;
}
 
const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case "SET_LOADING":
      return { ...state, loading: true };
    case "SET_DATA":
      return { ...state, loading: false, data: action.payload };
    case "SET_ERROR":
      return { ...state, loading: false, error: action.payload };
  }
};

Custom Hooks

Type-Safe Custom Hooks

interface UseApiResponse<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
}
 
function useApi<T>(url: string): UseApiResponse<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
 
  useEffect(() => {
    fetchData();
  }, [url]);
 
  const fetchData = async () => {
    try {
      const response = await fetch(url);
      const json = await response.json();
      setData(json);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };
 
  return { data, loading, error };
}

Event Handling

Typing Event Handlers

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  console.log(event.target.value);
};
 
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
  event.preventDefault();
  // Form submission logic
};

Best Practices

Use Type Assertions Sparingly

Only use type assertions when you know more about the type than TypeScript does:

// Avoid
const element = document.getElementById("root") as HTMLElement;
 
// Better
const element = document.getElementById("root");
if (element === null) throw new Error("Root element not found");

Leverage Utility Types

TypeScript provides several utility types that are helpful in React development:

// Make all properties optional
type PartialUser = Partial<User>;
 
// Make all properties required
type RequiredUser = Required<User>;
 
// Pick specific properties
type UserNameAndEmail = Pick<User, "name" | "email">;
 
// Omit specific properties
type UserWithoutId = Omit<User, "id">;

Conclusion

Using TypeScript with React not only provides better type safety but also improves the development experience through better tooling and documentation. By following these patterns and best practices, you can write more maintainable and robust React applications.

Indra Arianggi

Indra Arianggi