blog/4
TypeScript7 min read

TypeScript Tips for React Developers

By Marin Cholakov11/28/2024
TypeScriptReactJavaScriptDevelopment
TypeScript Tips for React Developers

TypeScript and React make a powerful combination. Here are essential tips to level up your TypeScript skills in React projects:

1. Component Props with TypeScript

Always type your component props:

interface ButtonProps {
  variant: 'primary' | 'secondary' | 'danger';
  size?: 'small' | 'medium' | 'large';
  onClick: () => void;
  children: React.ReactNode;
  disabled?: boolean;
}

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

2. Generic Components

Create reusable components with generics:

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

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

3. Custom Hooks with TypeScript

function useLocalStorage<T>(
  key: string, 
  initialValue: T
): [T, (value: T) => void] {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      return initialValue;
    }
  });

  const setValue = (value: T) => {
    try {
      setStoredValue(value);
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error(error);
    }
  };

  return [storedValue, setValue];
}

4. Event Handlers

Properly type event handlers:

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  setValue(e.target.value);
};

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
  // Handle form submission
};

5. Utility Types

Leverage TypeScript utility types:

// Extract props from existing components
type DivProps = React.ComponentProps<'div'>;

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

// Pick specific properties
type UserSummary = Pick<User, 'id' | 'name' | 'email'>;

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

6. Advanced Type Patterns

Conditional Types

type ApiResponse<T> = T extends string
  ? { message: T }
  : { data: T };

// Usage
type StringResponse = ApiResponse<string>; // { message: string }
type UserResponse = ApiResponse<User>; // { data: User }

Mapped Types

type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

// Make email optional in User type
type UserWithOptionalEmail = Optional<User, 'email'>;

Template Literal Types

type EventName = `on${Capitalize<string>}`;
type ButtonEvent = `button${Capitalize<'click' | 'hover' | 'focus'>}`;
// Results in: 'buttonClick' | 'buttonHover' | 'buttonFocus'

7. Form Handling with TypeScript

interface FormData {
  name: string;
  email: string;
  age: number;
}

const useForm = <T extends Record<string, any>>(
  initialValues: T,
  validate: (values: T) => Partial<Record<keyof T, string>>
) => {
  const [values, setValues] = useState<T>(initialValues);
  const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});

  const handleChange = (field: keyof T) => (
    e: React.ChangeEvent<HTMLInputElement>
  ) => {
    const value = e.target.type === 'number' ? +e.target.value : e.target.value;
    setValues(prev => ({ ...prev, [field]: value }));
  };

  const handleSubmit = (onSubmit: (values: T) => void) => (
    e: React.FormEvent
  ) => {
    e.preventDefault();
    const newErrors = validate(values);
    setErrors(newErrors);
    
    if (Object.keys(newErrors).length === 0) {
      onSubmit(values);
    }
  };

  return { values, errors, handleChange, handleSubmit };
};

8. Best Practices Summary

  1. Always type your props - prevents runtime errors
  2. Use strict TypeScript config - enable strict mode
  3. Leverage utility types - avoid repetitive type definitions
  4. Create reusable type definitions - share types across components
  5. Use generics for flexibility - make components reusable
  6. Type your hooks - ensure proper return types
  7. Handle async operations properly - use proper Promise types
  8. Use discriminated unions - for complex state types

These patterns will help you write more robust and maintainable React applications with TypeScript.

Share this post