FoundryKit

Best Practices

Best practices for using FoundryKit types effectively

Best Practices

Learn the best practices for using FoundryKit types to create maintainable, type-safe, and scalable applications.

Type Definition Best Practices

Use Descriptive Type Names

Create clear, descriptive names for your types that convey their purpose.

// ✅ Good: Clear and descriptive
interface UserProfileFormData {
  personalInfo: {
    firstName: string
    lastName: string
    email: string
  }
  preferences: {
    theme: 'light' | 'dark'
    notifications: boolean
  }
}

interface CreateUserRequest {
  name: string
  email: string
  age: number
}

interface UserApiResponse extends ApiResponse<User> {
  user: User
}

// ❌ Avoid: Vague or unclear names
interface Data {
  info: any
  settings: any
}

interface Request {
  stuff: any
}

interface Response {
  result: any
}

Prefer Union Types over Enums

Use union types for better type inference and tree-shaking.

// ✅ Good: Union types with const assertions
const USER_ROLES = ['admin', 'user', 'guest'] as const
type UserRole = typeof USER_ROLES[number] // 'admin' | 'user' | 'guest'

const THEME_OPTIONS = ['light', 'dark', 'auto'] as const
type Theme = typeof THEME_OPTIONS[number] // 'light' | 'dark' | 'auto'

// ✅ Good: Direct union types
type Status = 'pending' | 'approved' | 'rejected'
type Size = 'sm' | 'md' | 'lg'

// ❌ Avoid: Enums (unless you need reverse mapping)
enum UserRole {
  ADMIN = 'admin',
  USER = 'user',
  GUEST = 'guest'
}

Use Generic Constraints Effectively

Constrain generics to provide better type safety and IntelliSense.

// ✅ Good: Well-constrained generics
interface Repository<T extends { id: string }> {
  findById(id: string): Promise<T | null>
  create(data: Omit<T, 'id'>): Promise<T>
  update(id: string, data: Partial<Omit<T, 'id'>>): Promise<T | null>
  delete(id: string): Promise<boolean>
}

interface EventHandler<T extends Record<string, any> = {}> {
  type: string
  payload: T
  timestamp: Date
}

// Component props with constraints
interface ListProps<T extends { id: string | number }> {
  items: T[]
  renderItem: (item: T) => React.ReactNode
  keyExtractor?: (item: T) => string | number
}

// ❌ Avoid: Unconstrained generics
interface BadRepository<T> {
  findById(id: any): Promise<T | null>
  create(data: any): Promise<T>
}

interface BadEventHandler<T> {
  type: any
  payload: T
}

Component Type Patterns

Consistent Component Props Structure

Establish consistent patterns for component props.

import type { ComponentProps, WithChildren, WithClassName } from '@foundrykit/types'

// Base component props pattern
interface BaseComponentProps extends WithChildren, WithClassName {
  id?: string
  'data-testid'?: string
}

// Interactive component props pattern
interface InteractiveComponentProps extends BaseComponentProps {
  disabled?: boolean
  loading?: boolean
  onClick?: EventHandler<React.MouseEvent>
  onKeyDown?: EventHandler<React.KeyboardEvent>
}

// Form component props pattern
interface FormComponentProps extends InteractiveComponentProps {
  name?: string
  required?: boolean
  error?: string
  helperText?: string
}

// Specific component implementation
interface ButtonProps extends InteractiveComponentProps {
  variant?: 'primary' | 'secondary' | 'danger'
  size?: 'sm' | 'md' | 'lg'
  type?: 'button' | 'submit' | 'reset'
  leftIcon?: React.ReactNode
  rightIcon?: React.ReactNode
}

// Usage with consistent patterns
function Button({
  variant = 'primary',
  size = 'md',
  type = 'button',
  disabled = false,
  loading = false,
  className,
  children,
  leftIcon,
  rightIcon,
  onClick,
  ...props
}: ButtonProps) {
  return (
    <button
      type={type}
      disabled={disabled || loading}
      className={cn(
        'btn',
        `btn-${variant}`,
        `btn-${size}`,
        { 'btn-disabled': disabled, 'btn-loading': loading },
        className
      )}
      onClick={onClick}
      {...props}
    >
      {leftIcon && <span className="btn-icon-left">{leftIcon}</span>}
      {loading ? 'Loading...' : children}
      {rightIcon && <span className="btn-icon-right">{rightIcon}</span>}
    </button>
  )
}

Polymorphic Component Patterns

Create flexible polymorphic components with proper type safety.

import type { ComponentProps, PolymorphicComponent } from '@foundrykit/types'

// Polymorphic component with constraints
type PolymorphicTextElement = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'span' | 'div'

interface TextProps {
  size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
  weight?: 'normal' | 'medium' | 'semibold' | 'bold'
  color?: 'primary' | 'secondary' | 'muted' | 'error' | 'success'
  align?: 'left' | 'center' | 'right'
  truncate?: boolean
}

const Text: PolymorphicComponent<'p', TextProps> = ({
  as: Component = 'p',
  size = 'md',
  weight = 'normal',
  color = 'primary',
  align = 'left',
  truncate = false,
  className,
  children,
  ...props
}) => {
  return (
    <Component
      className={cn(
        'text',
        `text-${size}`,
        `text-weight-${weight}`,
        `text-${color}`,
        `text-${align}`,
        { 'text-truncate': truncate },
        className
      )}
      {...props}
    >
      {children}
    </Component>
  )
}

// Usage with full type safety
function TextExamples() {
  return (
    <div>
      {/* Default paragraph */}
      <Text>Default paragraph text</Text>
      
      {/* Heading with specific props */}
      <Text as="h1" size="xl" weight="bold" color="primary">
        Main heading
      </Text>
      
      {/* Span with truncation */}
      <Text as="span" size="sm" color="muted" truncate>
        This text will be truncated if too long
      </Text>
      
      {/* All HTML props are properly typed based on 'as' prop */}
      <Text as="button" onClick={() => console.log('clicked')}>
        Text as button (has onClick)
      </Text>
    </div>
  )
}

API Type Patterns

Consistent API Response Structure

Establish consistent patterns for API responses.

import type { ApiResponse, ApiError, PaginatedResponse } from '@foundrykit/types'

// Standard API response patterns
interface StandardApiResponse<T> extends ApiResponse<T> {
  data: T
  success: true
  message?: string
  timestamp: string
  requestId?: string
}

interface StandardApiError extends ApiError {
  error: true
  code: string
  message: string
  details?: Record<string, any>
  timestamp: string
  requestId?: string
  path?: string
}

// Specific API response types
type GetUserResponse = StandardApiResponse<User>
type GetUsersResponse = PaginatedResponse<User>
type CreateUserResponse = StandardApiResponse<User>
type UpdateUserResponse = StandardApiResponse<User>
type DeleteUserResponse = StandardApiResponse<{ success: boolean }>

// API client with consistent error handling
class ApiClient {
  private async request<T>(
    endpoint: string,
    options: RequestInit = {}
  ): Promise<T> {
    try {
      const response = await fetch(endpoint, {
        headers: {
          'Content-Type': 'application/json',
          ...options.headers,
        },
        ...options,
      })

      const data = await response.json()

      if (!response.ok) {
        const error: StandardApiError = {
          error: true,
          code: data.code || 'HTTP_ERROR',
          message: data.message || response.statusText,
          details: data.details,
          timestamp: data.timestamp || new Date().toISOString(),
          requestId: data.requestId,
          path: endpoint
        }
        throw error
      }

      return data
    } catch (error) {
      if (error && typeof error === 'object' && 'error' in error) {
        throw error // Re-throw API errors
      }
      
      // Convert unknown errors to standard format
      const standardError: StandardApiError = {
        error: true,
        code: 'NETWORK_ERROR',
        message: error instanceof Error ? error.message : 'Unknown error',
        timestamp: new Date().toISOString(),
        path: endpoint
      }
      throw standardError
    }
  }

  async getUser(id: string): Promise<GetUserResponse> {
    return this.request<GetUserResponse>(`/api/users/${id}`)
  }

  async getUsers(params?: {
    page?: number
    limit?: number
    search?: string
  }): Promise<GetUsersResponse> {
    const searchParams = new URLSearchParams()
    if (params?.page) searchParams.set('page', String(params.page))
    if (params?.limit) searchParams.set('limit', String(params.limit))
    if (params?.search) searchParams.set('search', params.search)
    
    const query = searchParams.toString()
    return this.request<GetUsersResponse>(`/api/users${query ? `?${query}` : ''}`)
  }

  async createUser(userData: CreateUserRequest): Promise<CreateUserResponse> {
    return this.request<CreateUserResponse>('/api/users', {
      method: 'POST',
      body: JSON.stringify(userData),
    })
  }
}

Type-safe API Error Handling

Create comprehensive error handling patterns.

// Error type hierarchy
interface BaseApiError extends ApiError {
  error: true
  code: string
  message: string
  timestamp: string
}

interface ValidationError extends BaseApiError {
  code: 'VALIDATION_ERROR'
  details: {
    field: string
    rule: string
    value: any
    message: string
  }[]
}

interface NotFoundError extends BaseApiError {
  code: 'NOT_FOUND'
  details: {
    resource: string
    id: string
  }
}

interface UnauthorizedError extends BaseApiError {
  code: 'UNAUTHORIZED'
  details?: {
    reason: 'invalid_token' | 'expired_token' | 'missing_token'
  }
}

interface RateLimitError extends BaseApiError {
  code: 'RATE_LIMIT_EXCEEDED'
  details: {
    limit: number
    remaining: number
    resetTime: string
  }
}

type ApiErrorType = ValidationError | NotFoundError | UnauthorizedError | RateLimitError

// Type guards for error handling
function isValidationError(error: ApiErrorType): error is ValidationError {
  return error.code === 'VALIDATION_ERROR'
}

function isNotFoundError(error: ApiErrorType): error is NotFoundError {
  return error.code === 'NOT_FOUND'
}

function isUnauthorizedError(error: ApiErrorType): error is UnauthorizedError {
  return error.code === 'UNAUTHORIZED'
}

function isRateLimitError(error: ApiErrorType): error is RateLimitError {
  return error.code === 'RATE_LIMIT_EXCEEDED'
}

// Error handling service
class ErrorHandlingService {
  static handleApiError(error: ApiErrorType): string {
    if (isValidationError(error)) {
      const fieldErrors = error.details.map(detail => 
        `${detail.field}: ${detail.message}`
      ).join(', ')
      return `Validation failed: ${fieldErrors}`
    }
    
    if (isNotFoundError(error)) {
      return `${error.details.resource} with ID ${error.details.id} not found`
    }
    
    if (isUnauthorizedError(error)) {
      switch (error.details?.reason) {
        case 'expired_token':
          return 'Your session has expired. Please log in again.'
        case 'invalid_token':
          return 'Invalid authentication token. Please log in again.'
        case 'missing_token':
          return 'Authentication required. Please log in.'
        default:
          return 'You are not authorized to perform this action.'
      }
    }
    
    if (isRateLimitError(error)) {
      return `Rate limit exceeded. Try again after ${error.details.resetTime}`
    }
    
    return error.message || 'An unexpected error occurred'
  }

  static getErrorSeverity(error: ApiErrorType): 'low' | 'medium' | 'high' | 'critical' {
    switch (error.code) {
      case 'VALIDATION_ERROR':
        return 'low'
      case 'NOT_FOUND':
        return 'medium'
      case 'UNAUTHORIZED':
        return 'high'
      case 'RATE_LIMIT_EXCEEDED':
        return 'medium'
      default:
        return 'critical'
    }
  }
}

// Usage in components
function UserForm() {
  const [error, setError] = useState<ApiErrorType | null>(null)

  const handleSubmit = async (userData: CreateUserRequest) => {
    try {
      await apiClient.createUser(userData)
      // Handle success
    } catch (err) {
      const apiError = err as ApiErrorType
      setError(apiError)
      
      // Log error with severity
      const severity = ErrorHandlingService.getErrorSeverity(apiError)
      console.error(`API Error [${severity}]:`, apiError)
    }
  }

  return (
    <div>
      {error && (
        <div className={`alert alert-${ErrorHandlingService.getErrorSeverity(error)}`}>
          {ErrorHandlingService.handleApiError(error)}
        </div>
      )}
      {/* Form content */}
    </div>
  )
}

Form Type Patterns

Comprehensive Form Type System

Create a robust form type system with validation.

import type { FormField, ValidationResult, FieldValidator } from '@foundrykit/types'

// Form field configuration
interface FormFieldConfig<T = any> {
  defaultValue: T
  validation?: {
    required?: boolean
    minLength?: number
    maxLength?: number
    min?: number
    max?: number
    pattern?: RegExp
    custom?: FieldValidator<T>
  }
  label?: string
  placeholder?: string
  helpText?: string
}

// Form schema definition
interface FormSchema<T extends Record<string, any>> {
  [K in keyof T]: FormFieldConfig<T[K]>
}

// User form example
interface UserFormData {
  name: string
  email: string
  age: number
  bio: string
  terms: boolean
}

const userFormSchema: FormSchema<UserFormData> = {
  name: {
    defaultValue: '',
    validation: {
      required: true,
      minLength: 2,
      maxLength: 50
    },
    label: 'Full Name',
    placeholder: 'Enter your full name'
  },
  email: {
    defaultValue: '',
    validation: {
      required: true,
      pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    },
    label: 'Email Address',
    placeholder: 'Enter your email'
  },
  age: {
    defaultValue: 0,
    validation: {
      required: true,
      min: 18,
      max: 120
    },
    label: 'Age',
    placeholder: 'Enter your age'
  },
  bio: {
    defaultValue: '',
    validation: {
      maxLength: 500
    },
    label: 'Bio',
    placeholder: 'Tell us about yourself',
    helpText: 'Maximum 500 characters'
  },
  terms: {
    defaultValue: false,
    validation: {
      required: true,
      custom: (value: boolean) => ({
        isValid: value === true,
        error: value ? undefined : 'You must accept the terms and conditions'
      })
    },
    label: 'I accept the terms and conditions'
  }
}

// Form validation service
class FormValidationService {
  static validateField<T>(
    value: T,
    config: FormFieldConfig<T>
  ): { isValid: boolean; error?: string } {
    const { validation } = config
    if (!validation) return { isValid: true }

    // Required validation
    if (validation.required && (value === null || value === undefined || value === '')) {
      return { isValid: false, error: `${config.label || 'Field'} is required` }
    }

    // String validations
    if (typeof value === 'string') {
      if (validation.minLength && value.length < validation.minLength) {
        return {
          isValid: false,
          error: `${config.label || 'Field'} must be at least ${validation.minLength} characters`
        }
      }
      
      if (validation.maxLength && value.length > validation.maxLength) {
        return {
          isValid: false,
          error: `${config.label || 'Field'} must be no more than ${validation.maxLength} characters`
        }
      }
      
      if (validation.pattern && !validation.pattern.test(value)) {
        return {
          isValid: false,
          error: `${config.label || 'Field'} format is invalid`
        }
      }
    }

    // Number validations
    if (typeof value === 'number') {
      if (validation.min !== undefined && value < validation.min) {
        return {
          isValid: false,
          error: `${config.label || 'Field'} must be at least ${validation.min}`
        }
      }
      
      if (validation.max !== undefined && value > validation.max) {
        return {
          isValid: false,
          error: `${config.label || 'Field'} must be no more than ${validation.max}`
        }
      }
    }

    // Custom validation
    if (validation.custom) {
      return validation.custom(value)
    }

    return { isValid: true }
  }

  static validateForm<T extends Record<string, any>>(
    data: T,
    schema: FormSchema<T>
  ): ValidationResult<T> {
    const errors: Partial<Record<keyof T, string>> = {}

    for (const [field, config] of Object.entries(schema) as Array<[keyof T, FormFieldConfig]>) {
      const validation = this.validateField(data[field], config)
      if (!validation.isValid && validation.error) {
        errors[field] = validation.error
      }
    }

    const isValid = Object.keys(errors).length === 0

    return {
      isValid,
      errors: errors as Record<keyof T, string>,
      data: isValid ? data : null
    }
  }
}

// Type-safe form hook
function useTypedForm<T extends Record<string, any>>(
  schema: FormSchema<T>
) {
  const [data, setData] = useState<T>(() => {
    const initialData = {} as T
    for (const [field, config] of Object.entries(schema) as Array<[keyof T, FormFieldConfig]>) {
      initialData[field] = config.defaultValue
    }
    return initialData
  })

  const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({})
  const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({})

  const updateField = useCallback(<K extends keyof T>(field: K, value: T[K]) => {
    setData(prev => ({ ...prev, [field]: value }))
    
    // Validate field on change if it's been touched
    if (touched[field]) {
      const validation = FormValidationService.validateField(value, schema[field])
      setErrors(prev => ({
        ...prev,
        [field]: validation.error
      }))
    }
  }, [schema, touched])

  const touchField = useCallback(<K extends keyof T>(field: K) => {
    setTouched(prev => ({ ...prev, [field]: true }))
    
    // Validate field when touched
    const validation = FormValidationService.validateField(data[field], schema[field])
    if (!validation.isValid) {
      setErrors(prev => ({ ...prev, [field]: validation.error }))
    }
  }, [data, schema])

  const validateForm = useCallback(() => {
    const validation = FormValidationService.validateForm(data, schema)
    setErrors(validation.errors)
    return validation
  }, [data, schema])

  const reset = useCallback(() => {
    const initialData = {} as T
    for (const [field, config] of Object.entries(schema) as Array<[keyof T, FormFieldConfig]>) {
      initialData[field] = config.defaultValue
    }
    setData(initialData)
    setErrors({})
    setTouched({})
  }, [schema])

  return {
    data,
    errors,
    touched,
    updateField,
    touchField,
    validateForm,
    reset,
    isValid: Object.keys(errors).length === 0
  }
}

// Usage
function UserForm() {
  const {
    data,
    errors,
    touched,
    updateField,
    touchField,
    validateForm,
    reset,
    isValid
  } = useTypedForm(userFormSchema)

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    
    const validation = validateForm()
    if (validation.isValid && validation.data) {
      try {
        await apiClient.createUser(validation.data)
        reset()
      } catch (error) {
        console.error('Failed to create user:', error)
      }
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>{userFormSchema.name.label}</label>
        <input
          value={data.name}
          onChange={(e) => updateField('name', e.target.value)}
          onBlur={() => touchField('name')}
          placeholder={userFormSchema.name.placeholder}
        />
        {touched.name && errors.name && (
          <span className="error">{errors.name}</span>
        )}
      </div>

      <div>
        <label>{userFormSchema.email.label}</label>
        <input
          type="email"
          value={data.email}
          onChange={(e) => updateField('email', e.target.value)}
          onBlur={() => touchField('email')}
          placeholder={userFormSchema.email.placeholder}
        />
        {touched.email && errors.email && (
          <span className="error">{errors.email}</span>
        )}
      </div>

      <div>
        <label>{userFormSchema.age.label}</label>
        <input
          type="number"
          value={data.age}
          onChange={(e) => updateField('age', parseInt(e.target.value) || 0)}
          onBlur={() => touchField('age')}
          placeholder={userFormSchema.age.placeholder}
        />
        {touched.age && errors.age && (
          <span className="error">{errors.age}</span>
        )}
      </div>

      <div>
        <label>{userFormSchema.bio.label}</label>
        <textarea
          value={data.bio}
          onChange={(e) => updateField('bio', e.target.value)}
          onBlur={() => touchField('bio')}
          placeholder={userFormSchema.bio.placeholder}
        />
        {userFormSchema.bio.helpText && (
          <small>{userFormSchema.bio.helpText}</small>
        )}
        {touched.bio && errors.bio && (
          <span className="error">{errors.bio}</span>
        )}
      </div>

      <div>
        <label>
          <input
            type="checkbox"
            checked={data.terms}
            onChange={(e) => updateField('terms', e.target.checked)}
            onBlur={() => touchField('terms')}
          />
          {userFormSchema.terms.label}
        </label>
        {touched.terms && errors.terms && (
          <span className="error">{errors.terms}</span>
        )}
      </div>

      <button type="submit" disabled={!isValid}>
        Submit
      </button>
      
      <button type="button" onClick={reset}>
        Reset
      </button>
    </form>
  )
}

Testing Best Practices

Type-safe Test Utilities

Create comprehensive testing utilities with full type safety.

import type { ApiResponse, ApiError, ValidationResult } from '@foundrykit/types'

// Test data factories
interface TestDataFactory<T> {
  create(overrides?: Partial<T>): T
  createMany(count: number, overrides?: Partial<T>): T[]
}

class UserTestFactory implements TestDataFactory<User> {
  private static defaultUser: User = {
    id: '1',
    name: 'John Doe',
    email: 'john.doe@example.com',
    age: 30,
    createdAt: new Date(),
    updatedAt: new Date()
  }

  create(overrides: Partial<User> = {}): User {
    return {
      ...UserTestFactory.defaultUser,
      ...overrides,
      id: overrides.id || `user-${Math.random().toString(36).substr(2, 9)}`
    }
  }

  createMany(count: number, overrides: Partial<User> = {}): User[] {
    return Array.from({ length: count }, (_, index) =>
      this.create({ ...overrides, name: `${overrides.name || 'User'} ${index + 1}` })
    )
  }
}

// Mock API response factories
class ApiResponseFactory {
  static success<T>(data: T, message?: string): ApiResponse<T> {
    return {
      data,
      success: true,
      message,
      timestamp: new Date().toISOString()
    }
  }

  static error(code: string, message: string, details?: any): ApiError {
    return {
      error: true,
      code,
      message,
      details,
      timestamp: new Date().toISOString()
    }
  }

  static paginated<T>(
    data: T[],
    page: number = 1,
    limit: number = 10,
    total?: number
  ): PaginatedResponse<T> {
    const actualTotal = total ?? data.length
    
    return {
      data,
      success: true,
      timestamp: new Date().toISOString(),
      pagination: {
        page,
        limit,
        total: actualTotal,
        totalPages: Math.ceil(actualTotal / limit),
        hasNextPage: page * limit < actualTotal,
        hasPrevPage: page > 1
      }
    }
  }
}

// Type-safe test assertions
class TypedAssertions {
  static isApiResponse<T>(response: any): asserts response is ApiResponse<T> {
    expect(response).toHaveProperty('data')
    expect(response).toHaveProperty('success', true)
    expect(response).toHaveProperty('timestamp')
    expect(typeof response.timestamp).toBe('string')
  }

  static isApiError(error: any): asserts error is ApiError {
    expect(error).toHaveProperty('error', true)
    expect(error).toHaveProperty('code')
    expect(error).toHaveProperty('message')
    expect(error).toHaveProperty('timestamp')
    expect(typeof error.code).toBe('string')
    expect(typeof error.message).toBe('string')
  }

  static isPaginatedResponse<T>(response: any): asserts response is PaginatedResponse<T> {
    this.isApiResponse(response)
    expect(response).toHaveProperty('pagination')
    expect(response.pagination).toHaveProperty('page')
    expect(response.pagination).toHaveProperty('limit')
    expect(response.pagination).toHaveProperty('total')
    expect(response.pagination).toHaveProperty('totalPages')
    expect(response.pagination).toHaveProperty('hasNextPage')
    expect(response.pagination).toHaveProperty('hasPrevPage')
  }

  static isValidationResult<T>(result: any): asserts result is ValidationResult<T> {
    expect(result).toHaveProperty('isValid')
    expect(result).toHaveProperty('errors')
    expect(typeof result.isValid).toBe('boolean')
    expect(typeof result.errors).toBe('object')
  }
}

// Usage in tests
describe('User API', () => {
  const userFactory = new UserTestFactory()

  it('should return paginated users', async () => {
    const mockUsers = userFactory.createMany(5)
    const mockResponse = ApiResponseFactory.paginated(mockUsers, 1, 10, 25)

    global.fetch = vi.fn().mockResolvedValueOnce({
      ok: true,
      json: () => Promise.resolve(mockResponse)
    })

    const apiClient = new UserApiClient()
    const response = await apiClient.getUsers({ page: 1, limit: 10 })

    // Type-safe assertions
    TypedAssertions.isPaginatedResponse(response)
    expect(response.data).toHaveLength(5)
    expect(response.pagination.page).toBe(1)
    expect(response.pagination.total).toBe(25)
  })

  it('should handle API errors correctly', async () => {
    const mockError = ApiResponseFactory.error('NOT_FOUND', 'User not found')

    global.fetch = vi.fn().mockResolvedValueOnce({
      ok: false,
      json: () => Promise.resolve(mockError)
    })

    const apiClient = new UserApiClient()

    await expect(apiClient.getUser('invalid-id')).rejects.toThrow()
    
    try {
      await apiClient.getUser('invalid-id')
    } catch (error) {
      TypedAssertions.isApiError(error)
      expect(error.code).toBe('NOT_FOUND')
      expect(error.message).toBe('User not found')
    }
  })
})

describe('Form Validation', () => {
  const userFactory = new UserTestFactory()

  it('should validate user data correctly', () => {
    const validUser = userFactory.create()
    const result = FormValidationService.validateForm(validUser, userFormSchema)

    TypedAssertions.isValidationResult(result)
    expect(result.isValid).toBe(true)
    expect(Object.keys(result.errors)).toHaveLength(0)
    expect(result.data).toEqual(validUser)
  })

  it('should return validation errors for invalid data', () => {
    const invalidUser = userFactory.create({
      name: '', // Invalid: required field
      email: 'invalid-email', // Invalid: wrong format
      age: 15 // Invalid: below minimum
    })

    const result = FormValidationService.validateForm(invalidUser, userFormSchema)

    TypedAssertions.isValidationResult(result)
    expect(result.isValid).toBe(false)
    expect(result.errors.name).toBeTruthy()
    expect(result.errors.email).toBeTruthy()
    expect(result.errors.age).toBeTruthy()
    expect(result.data).toBeNull()
  })
})

Common Anti-Patterns

What to Avoid

// ❌ Don't use 'any' type
interface BadApiResponse {
  data: any
  status: any
}

// ❌ Don't create overly complex generic constraints
interface BadGeneric<T extends Record<string, Record<string, Record<string, any>>>> {
  data: T
}

// ❌ Don't ignore null/undefined possibilities
interface BadUser {
  id: string
  name: string
  email: string
  avatar: string // Should be optional or nullable
}

// ❌ Don't create inconsistent naming patterns
interface user_data {
  UserID: string
  user_name: string
  Email: string
}

// ❌ Don't use enums when union types are better
enum BadStatus {
  LOADING = 'loading',
  SUCCESS = 'success',
  ERROR = 'error'
}

// ❌ Don't create types that are too specific
interface VerySpecificButtonProps {
  isBlueRoundedButtonWithShadowAndHover: boolean
}

// ✅ Better alternatives
interface ApiResponse<T> {
  data: T
  success: boolean
  message?: string
}

interface SimpleGeneric<T extends Record<string, unknown>> {
  data: T
}

interface User {
  id: string
  name: string
  email: string
  avatar?: string | null
}

interface UserData {
  id: string
  name: string
  email: string
}

type Status = 'loading' | 'success' | 'error'

interface ButtonProps {
  variant: 'primary' | 'secondary' | 'danger'
  size: 'sm' | 'md' | 'lg'
  rounded?: boolean
  shadow?: boolean
}

Best Practices Checklist

Type Definition Checklist

  • Use descriptive, clear type names
  • Prefer union types over enums
  • Use generic constraints appropriately
  • Make optional properties explicit
  • Avoid using any type
  • Use consistent naming conventions
  • Document complex types with comments

Component Type Checklist

  • Extend base component prop types
  • Use polymorphic patterns when appropriate
  • Implement proper ref forwarding
  • Handle event types correctly
  • Use consistent prop patterns
  • Provide default values for optional props

API Type Checklist

  • Use consistent response structures
  • Implement proper error typing
  • Use type guards for error handling
  • Handle pagination consistently
  • Type request/response pairs together
  • Document API contracts with types

Form Type Checklist

  • Use comprehensive validation types
  • Implement field-level validation
  • Handle form state properly
  • Use type-safe form hooks
  • Provide clear error messages
  • Handle async validation

Testing Type Checklist

  • Create type-safe test utilities
  • Use proper assertion types
  • Mock API responses with correct types
  • Test type guards and predicates
  • Use factory patterns for test data

Next Steps