Back to Blog

Essential TypeScript Tips for React Developers

Adeel Imran
Adeel Imran

TypeScript has become essential for building large-scale React applications. In this post, I'll share some advanced TypeScript tips that have significantly improved my development workflow.

1. Proper Component Typing

Instead of using React.FC, I prefer explicit prop typing:

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

export function Button({ label, onClick, variant = "primary" }: ButtonProps) {
  return <button onClick={onClick}>{label}</button>;
}

This approach gives you more control and better type inference.

2. Utility Types Are Your Friends

TypeScript's built-in utility types are powerful:

// Pick specific properties
type UserBasicInfo = Pick<User, "name" | "email">;

// Omit properties
type UserWithoutPassword = Omit<User, "password">;

// Make all properties optional
type PartialUser = Partial<User>;

// Make all properties required
type RequiredUser = Required<User>;

3. Discriminated Unions for State

Handle complex state with discriminated unions:

type LoadingState =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: Data }
  | { status: "error"; error: Error };

function handleState(state: LoadingState) {
  switch (state.status) {
    case "idle":
      return <div>Ready to load</div>;
    case "loading":
      return <div>Loading...</div>;
    case "success":
      return <div>{state.data}</div>; // TypeScript knows data exists
    case "error":
      return <div>{state.error.message}</div>; // TypeScript knows error exists
  }
}

4. 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(renderItem)}</ul>;
}

// Usage with full type safety
<List items={users} renderItem={(user) => <li>{user.name}</li>} />;

5. Type Guards

Implement type guards for better type narrowing:

function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "name" in value &&
    "email" in value
  );
}

// Now TypeScript knows the type
if (isUser(data)) {
  console.log(data.name); // Type-safe!
}

6. As Const for Better Inference

Use as const for literal types:

const COLORS = ["red", "blue", "green"] as const;
type Color = (typeof COLORS)[number]; // 'red' | 'blue' | 'green'

7. Typed Event Handlers

Properly type event handlers:

function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
  console.log(event.target.value); // Fully typed
}

function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
  event.preventDefault();
  // Handle form submission
}

8. Extract Types from Components

Extract prop types when needed:

import { ComponentProps } from "react";

type ButtonProps = ComponentProps<"button">;
type DivProps = ComponentProps<"div">;

Best Practices

  1. Enable Strict Mode: Always use "strict": true in tsconfig.json
  2. Avoid Any: Use unknown instead of any when type is uncertain
  3. Use Type Inference: Let TypeScript infer types when possible
  4. Document Complex Types: Add JSDoc comments for complex type definitions
  5. Keep Types Close: Define types near where they're used for better maintainability

Conclusion

TypeScript significantly improves the development experience and code quality in React applications. These patterns have saved me countless hours of debugging and made my code more maintainable.

What are your favorite TypeScript patterns? Let me know on social media!