TypeScript Best Practices for Frontend Development

TypeScript Best Practices for Frontend Development

TypeScript JavaScript Frontend Best Practices

Essential TypeScript patterns and practices that every frontend developer should know for building maintainable applications.

TypeScript has become essential for modern frontend development. Here are the key practices that will make your TypeScript code more maintainable and robust.

Type Safety First

Use Strict Mode

Always enable strict mode in your tsconfig.json:

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

Prefer Type Annotations

Be explicit with your types, especially for function parameters and return values:

// Good
function calculateTotal(price: number, tax: number): number {
  return price + (price * tax);
}

// Avoid
function calculateTotal(price, tax) {
  return price + (price * tax);
}

Interface Design

Use Interfaces for Object Shapes

Interfaces are perfect for defining object structures:

interface User {
  readonly id: string;
  name: string;
  email: string;
  createdAt: Date;
  preferences?: UserPreferences;
}

interface UserPreferences {
  theme: 'light' | 'dark';
  notifications: boolean;
}

Extend Interfaces Wisely

Build upon existing interfaces rather than duplicating:

interface BaseEntity {
  id: string;
  createdAt: Date;
  updatedAt: Date;
}

interface User extends BaseEntity {
  name: string;
  email: string;
}

interface Post extends BaseEntity {
  title: string;
  content: string;
  authorId: string;
}

Utility Types

Leverage Built-in Utility Types

TypeScript provides powerful utility types:

// Partial - make all properties optional
type CreateUserInput = Partial<User>;

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

// Omit - exclude specific properties
type CreateUser = Omit<User, 'id' | 'createdAt'>;

// Record - create object type with specific keys
type UserRoles = Record<string, 'admin' | 'user' | 'guest'>;

Generic Patterns

Create Reusable Generic Types

Generics make your code more flexible:

interface ApiResponse<T> {
  data: T;
  success: boolean;
  message?: string;
}

interface PaginatedResponse<T> extends ApiResponse<T[]> {
  pagination: {
    page: number;
    limit: number;
    total: number;
  };
}

// Usage
type UsersResponse = PaginatedResponse<User>;
type PostResponse = ApiResponse<Post>;

React with TypeScript

Component Props

Always type your React component props:

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

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

Hooks with TypeScript

Type your custom hooks properly:

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

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

  useEffect(() => {
    // API call implementation
  }, [url]);

  return state;
}

Error Handling

Use Discriminated Unions for Results

Handle success and error states with discriminated unions:

type Result<T, E = Error> = 
  | { success: true; data: T }
  | { success: false; error: E };

async function fetchUser(id: string): Promise<Result<User>> {
  try {
    const user = await api.getUser(id);
    return { success: true, data: user };
  } catch (error) {
    return { success: false, error: error as Error };
  }
}

// Usage
const result = await fetchUser('123');
if (result.success) {
  console.log(result.data.name); // TypeScript knows this is User
} else {
  console.error(result.error.message); // TypeScript knows this is Error
}

Performance Tips

Use const Assertions

Preserve literal types with const assertions:

// Without const assertion
const themes = ['light', 'dark']; // Type: string[]

// With const assertion
const themes = ['light', 'dark'] as const; // Type: readonly ['light', 'dark']
type Theme = typeof themes[number]; // Type: 'light' | 'dark'

Lazy Load Types

For large applications, consider lazy loading types:

// types/index.ts
export type { User } from './user';
export type { Post } from './post';

// Use dynamic imports when needed
const getUserModule = () => import('./types/user');

TypeScript is a powerful tool that, when used correctly, can significantly improve your code quality and developer experience. These practices will help you build more maintainable and robust frontend applications.