Skip to content
All posts

December 10, 2023

TypeScript Best Practices for React Developers

A comprehensive guide to writing better TypeScript code in React applications, with practical examples and patterns.

TypeScript · React · Best Practices

Introduction

TypeScript has become the de facto standard for building robust React applications. In this guide, I'll share essential TypeScript best practices specifically tailored for React developers.

1. Properly Type Your Components

Always define explicit types for your component props:

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

const Button: React.FC<ButtonProps> = ({
  label,
  onClick,
  variant = 'primary',
  disabled = false,
}) => {
  return (
    <button onClick={onClick} disabled={disabled} className={variant}>
      {label}
    </button>
  );
};

2. Use Discriminated Unions

For state that can be in different modes, use discriminated unions:

type AsyncState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

function DataComponent() {
  const [state, setState] = useState<AsyncState<User>>({
    status: 'idle',
  });

  if (state.status === 'loading') return <Spinner />;
  if (state.status === 'error') return <Error error={state.error} />;
  if (state.status === 'success') return <UserProfile user={state.data} />;

  return null;
}

3. Leverage Type Inference

Don't over-specify types when TypeScript can infer them:

// ❌ Unnecessary
const [count, setCount] = useState<number>(0);

// ✅ Better - TypeScript infers the type
const [count, setCount] = useState(0);

// ✅ Good - When initial value doesn't match the full type
const [user, setUser] = useState<User | null>(null);

4. Use Generics for Reusable Components

Create flexible, type-safe components with generics:

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

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

// Usage
<List
  items={users}
  renderItem={(user) => <UserCard user={user} />}
  keyExtractor={(user) => user.id}
/>;

5. Strict Null Checks

Always enable strict null checks in `tsconfig.json`:

{
  "compilerOptions": {
    "strict": true,
    "strictNullChecks": true
  }
}

Handle nullable values explicitly:

// ❌ Unsafe
function greet(name: string | null) {
  return `Hello, ${name.toUpperCase()}`; // Error!
}

// ✅ Safe
function greet(name: string | null) {
  return `Hello, ${name?.toUpperCase() ?? 'Guest'}`;
}

6. Use Const Assertions

Make your objects and arrays more specific:

// Without const assertion
const colors = ['red', 'blue', 'green']; // string[]

// With const assertion
const colors = ['red', 'blue', 'green'] as const;
// readonly ['red', 'blue', 'green']

type Color = (typeof colors)[number]; // 'red' | 'blue' | 'green'

7. Avoid `any` - Use `unknown` Instead

When you truly don't know the type, use `unknown`:

// ❌ Bad
function processData(data: any) {
  return data.value; // No type safety
}

// ✅ Good
function processData(data: unknown) {
  if (typeof data === 'object' && data !== null && 'value' in data) {
    return (data as { value: string }).value;
  }
  throw new Error('Invalid data');
}

8. Create Utility Types

Build reusable type utilities:

// Make all properties optional recursively
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

// Extract component props type
type ButtonProps = React.ComponentProps<typeof Button>;

// Pick specific props
type UserNameProps = Pick<User, 'firstName' | 'lastName'>;

9. Type Your Hooks Properly

Custom hooks should have proper return types:

function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);

  const toggle = useCallback(() => setValue((v) => !v), []);
  const setTrue = useCallback(() => setValue(true), []);
  const setFalse = useCallback(() => setValue(false), []);

  return { value, toggle, setTrue, setFalse } as const;
}

// Usage with proper types
const { value, toggle } = useToggle();

10. Use Type Guards

Create type guards for runtime checks:

interface User {
  type: 'user';
  name: string;
}

interface Admin {
  type: 'admin';
  name: string;
  permissions: string[];
}

type Person = User | Admin;

function isAdmin(person: Person): person is Admin {
  return person.type === 'admin';
}

function greet(person: Person) {
  if (isAdmin(person)) {
    // TypeScript knows person is Admin here
    console.log(person.permissions);
  }
}

Conclusion

These TypeScript best practices will help you write more maintainable, type-safe React applications. Remember, TypeScript is there to help you catch errors early and provide better IDE support—embrace it!

Start implementing these patterns in your projects today, and you'll see immediate improvements in code quality and developer experience.

Happy coding! 💙