FoundryKit

Custom Type Patterns

Advanced patterns and techniques for creating custom types

Custom Type Patterns

Learn advanced patterns and techniques for creating custom types that enhance type safety and developer experience in your applications.

Generic Type Patterns

Generic Component Props

Create flexible, reusable component types with generics.

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

// Generic list component
interface ListProps<T> extends WithChildren {
  items: T[]
  renderItem: (item: T, index: number) => React.ReactNode
  keyExtractor: (item: T) => string | number
  className?: string
  emptyMessage?: string
}

function List<T>({ 
  items, 
  renderItem, 
  keyExtractor, 
  className, 
  emptyMessage = 'No items found',
  children 
}: ListProps<T>) {
  if (items.length === 0) {
    return <div className="empty-state">{emptyMessage}</div>
  }

  return (
    <div className={className}>
      {children}
      {items.map((item, index) => (
        <div key={keyExtractor(item)}>
          {renderItem(item, index)}
        </div>
      ))}
    </div>
  )
}

// Usage with type inference
interface User {
  id: string
  name: string
  email: string
}

function UserList({ users }: { users: User[] }) {
  return (
    <List
      items={users}
      keyExtractor={(user) => user.id}
      renderItem={(user, index) => (
        <div className="user-item">
          <h3>{user.name}</h3>
          <p>{user.email}</p>
        </div>
      )}
      emptyMessage="No users found"
    />
  )
}

Generic Data Fetching

Create type-safe data fetching patterns.

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

// Generic data fetching hook
interface UseDataOptions<T> {
  enabled?: boolean
  refetchOnWindowFocus?: boolean
  staleTime?: number
  cacheTime?: number
  onSuccess?: (data: T) => void
  onError?: (error: ApiError) => void
}

interface UseDataResult<T> {
  data: T | null
  loading: boolean
  error: ApiError | null
  refetch: () => Promise<void>
  mutate: (data: T) => void
}

function useData<T>(
  fetcher: () => Promise<ApiResponse<T>>,
  options: UseDataOptions<T> = {}
): UseDataResult<T> {
  const [data, setData] = useState<T | null>(null)
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState<ApiError | null>(null)

  const fetchData = useCallback(async () => {
    if (!options.enabled && options.enabled !== undefined) return

    setLoading(true)
    setError(null)

    try {
      const response = await fetcher()
      setData(response.data)
      options.onSuccess?.(response.data)
    } catch (err) {
      const apiError = err as ApiError
      setError(apiError)
      options.onError?.(apiError)
    } finally {
      setLoading(false)
    }
  }, [fetcher, options])

  const mutate = useCallback((newData: T) => {
    setData(newData)
  }, [])

  useEffect(() => {
    fetchData()
  }, [fetchData])

  return {
    data,
    loading,
    error,
    refetch: fetchData,
    mutate
  }
}

// Usage with type inference
interface User {
  id: string
  name: string
  email: string
}

function UserProfile({ userId }: { userId: string }) {
  const {
    data: user,
    loading,
    error,
    refetch
  } = useData<User>(
    () => fetchUser(userId),
    {
      enabled: !!userId,
      onSuccess: (user) => {
        console.log(`Loaded user: ${user.name}`)
      },
      onError: (error) => {
        console.error('Failed to load user:', error.message)
      }
    }
  )

  if (loading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>
  if (!user) return <div>User not found</div>

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <button onClick={refetch}>Refresh</button>
    </div>
  )
}

Builder Pattern Types

Fluent API Builder

Create type-safe builder patterns for complex configurations.

// Query builder pattern
interface QueryBuilder<T> {
  where<K extends keyof T>(field: K, operator: 'eq' | 'ne' | 'gt' | 'lt' | 'in', value: T[K] | T[K][]): QueryBuilder<T>
  orderBy<K extends keyof T>(field: K, direction?: 'asc' | 'desc'): QueryBuilder<T>
  limit(count: number): QueryBuilder<T>
  offset(count: number): QueryBuilder<T>
  build(): QueryConfig<T>
}

interface QueryConfig<T> {
  filters: Array<{
    field: keyof T
    operator: string
    value: any
  }>
  sorting: Array<{
    field: keyof T
    direction: 'asc' | 'desc'
  }>
  pagination: {
    limit?: number
    offset?: number
  }
}

class TypedQueryBuilder<T> implements QueryBuilder<T> {
  private config: QueryConfig<T> = {
    filters: [],
    sorting: [],
    pagination: {}
  }

  where<K extends keyof T>(
    field: K,
    operator: 'eq' | 'ne' | 'gt' | 'lt' | 'in',
    value: T[K] | T[K][]
  ): QueryBuilder<T> {
    this.config.filters.push({ field, operator, value })
    return this
  }

  orderBy<K extends keyof T>(
    field: K,
    direction: 'asc' | 'desc' = 'asc'
  ): QueryBuilder<T> {
    this.config.sorting.push({ field, direction })
    return this
  }

  limit(count: number): QueryBuilder<T> {
    this.config.pagination.limit = count
    return this
  }

  offset(count: number): QueryBuilder<T> {
    this.config.pagination.offset = count
    return this
  }

  build(): QueryConfig<T> {
    return { ...this.config }
  }
}

// Factory function
function createQuery<T>(): QueryBuilder<T> {
  return new TypedQueryBuilder<T>()
}

// Usage with full type safety
interface Product {
  id: string
  name: string
  price: number
  category: string
  inStock: boolean
}

const query = createQuery<Product>()
  .where('category', 'eq', 'electronics')
  .where('price', 'gt', 100)
  .where('inStock', 'eq', true)
  .orderBy('price', 'desc')
  .orderBy('name', 'asc')
  .limit(20)
  .offset(0)
  .build()

// Type-safe query execution
async function executeQuery<T>(config: QueryConfig<T>): Promise<T[]> {
  // Implementation would convert config to actual query
  console.log('Executing query:', config)
  return []
}

const products = await executeQuery<Product>(query)

Configuration Builder

Create type-safe configuration builders.

// Component configuration builder
interface ComponentConfig {
  theme: 'light' | 'dark'
  size: 'sm' | 'md' | 'lg'
  variant: 'primary' | 'secondary' | 'danger'
  disabled: boolean
  loading: boolean
  className?: string
}

interface ConfigBuilder<T> {
  set<K extends keyof T>(key: K, value: T[K]): ConfigBuilder<T>
  merge(partial: Partial<T>): ConfigBuilder<T>
  build(): T
}

class TypedConfigBuilder<T> implements ConfigBuilder<T> {
  constructor(private config: T) {}

  set<K extends keyof T>(key: K, value: T[K]): ConfigBuilder<T> {
    return new TypedConfigBuilder({ ...this.config, [key]: value })
  }

  merge(partial: Partial<T>): ConfigBuilder<T> {
    return new TypedConfigBuilder({ ...this.config, ...partial })
  }

  build(): T {
    return this.config
  }
}

// Factory with defaults
function createComponentConfig(): ConfigBuilder<ComponentConfig> {
  const defaults: ComponentConfig = {
    theme: 'light',
    size: 'md',
    variant: 'primary',
    disabled: false,
    loading: false
  }
  
  return new TypedConfigBuilder(defaults)
}

// Usage
const buttonConfig = createComponentConfig()
  .set('theme', 'dark')
  .set('size', 'lg')
  .set('variant', 'danger')
  .merge({ disabled: false, loading: true })
  .build()

// Use config in component
function ConfigurableButton({ config, children }: { 
  config: ComponentConfig
  children: React.ReactNode 
}) {
  return (
    <button
      className={cn(
        'btn',
        `btn-${config.theme}`,
        `btn-${config.size}`,
        `btn-${config.variant}`,
        {
          'btn-disabled': config.disabled,
          'btn-loading': config.loading
        },
        config.className
      )}
      disabled={config.disabled || config.loading}
    >
      {config.loading ? 'Loading...' : children}
    </button>
  )
}

State Machine Types

Finite State Machine

Create type-safe state machines.

// State machine definition
interface StateMachine<TState extends string, TEvent extends string> {
  initial: TState
  states: Record<TState, StateDefinition<TState, TEvent>>
}

interface StateDefinition<TState extends string, TEvent extends string> {
  on?: Partial<Record<TEvent, TState>>
  entry?: () => void
  exit?: () => void
}

interface StateContext<TState extends string, TEvent extends string> {
  current: TState
  send: (event: TEvent) => void
  can: (event: TEvent) => boolean
}

// Form state machine example
type FormState = 'idle' | 'validating' | 'submitting' | 'success' | 'error'
type FormEvent = 'VALIDATE' | 'SUBMIT' | 'SUCCESS' | 'ERROR' | 'RESET'

const formStateMachine: StateMachine<FormState, FormEvent> = {
  initial: 'idle',
  states: {
    idle: {
      on: {
        VALIDATE: 'validating',
        SUBMIT: 'submitting'
      }
    },
    validating: {
      on: {
        SUBMIT: 'submitting',
        ERROR: 'error',
        RESET: 'idle'
      }
    },
    submitting: {
      on: {
        SUCCESS: 'success',
        ERROR: 'error'
      },
      entry: () => console.log('Starting submission...'),
      exit: () => console.log('Submission completed')
    },
    success: {
      on: {
        RESET: 'idle'
      }
    },
    error: {
      on: {
        VALIDATE: 'validating',
        RESET: 'idle'
      }
    }
  }
}

// State machine hook
function useStateMachine<TState extends string, TEvent extends string>(
  machine: StateMachine<TState, TEvent>
): StateContext<TState, TEvent> {
  const [current, setCurrent] = useState<TState>(machine.initial)

  const send = useCallback((event: TEvent) => {
    const currentState = machine.states[current]
    const nextState = currentState.on?.[event]
    
    if (nextState) {
      // Call exit handler
      currentState.exit?.()
      
      // Transition to next state
      setCurrent(nextState)
      
      // Call entry handler
      machine.states[nextState].entry?.()
    }
  }, [current, machine])

  const can = useCallback((event: TEvent) => {
    const currentState = machine.states[current]
    return !!currentState.on?.[event]
  }, [current, machine])

  return { current, send, can }
}

// Usage in component
function FormWithStateMachine() {
  const { current, send, can } = useStateMachine(formStateMachine)
  const [formData, setFormData] = useState({ name: '', email: '' })

  const handleValidate = () => {
    if (can('VALIDATE')) {
      send('VALIDATE')
      // Perform validation logic
      setTimeout(() => {
        const isValid = formData.name && formData.email
        send(isValid ? 'RESET' : 'ERROR')
      }, 1000)
    }
  }

  const handleSubmit = () => {
    if (can('SUBMIT')) {
      send('SUBMIT')
      // Perform submission logic
      setTimeout(() => {
        const success = Math.random() > 0.5
        send(success ? 'SUCCESS' : 'ERROR')
      }, 2000)
    }
  }

  return (
    <div>
      <div>Current state: {current}</div>
      
      <input
        value={formData.name}
        onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
        placeholder="Name"
        disabled={current === 'submitting'}
      />
      
      <input
        value={formData.email}
        onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
        placeholder="Email"
        disabled={current === 'submitting'}
      />
      
      <button
        onClick={handleValidate}
        disabled={!can('VALIDATE')}
      >
        Validate
      </button>
      
      <button
        onClick={handleSubmit}
        disabled={!can('SUBMIT')}
      >
        {current === 'submitting' ? 'Submitting...' : 'Submit'}
      </button>
      
      <button
        onClick={() => send('RESET')}
        disabled={!can('RESET')}
      >
        Reset
      </button>
      
      {current === 'success' && (
        <div className="success">Form submitted successfully!</div>
      )}
      
      {current === 'error' && (
        <div className="error">Please check your inputs and try again.</div>
      )}
    </div>
  )
}

Plugin System Types

Extensible Plugin Architecture

Create type-safe plugin systems.

// Plugin system types
interface Plugin<TConfig = unknown, TApi = unknown> {
  name: string
  version: string
  config?: TConfig
  install: (api: TApi) => void | Promise<void>
  uninstall?: (api: TApi) => void | Promise<void>
}

interface PluginManager<TApi> {
  register<TConfig>(plugin: Plugin<TConfig, TApi>): Promise<void>
  unregister(name: string): Promise<void>
  getPlugin(name: string): Plugin<unknown, TApi> | undefined
  getPlugins(): Plugin<unknown, TApi>[]
}

// Example: Form validation plugin system
interface FormValidationApi {
  addValidator: (name: string, validator: FieldValidator) => void
  removeValidator: (name: string) => void
  validate: (value: any, rules: string[]) => ValidationResult
}

interface FieldValidator {
  validate: (value: any, options?: any) => { isValid: boolean; error?: string }
}

interface ValidationResult {
  isValid: boolean
  errors: string[]
}

// Email validation plugin
interface EmailValidatorConfig {
  allowInternational?: boolean
  requireTLD?: boolean
}

const emailValidatorPlugin: Plugin<EmailValidatorConfig, FormValidationApi> = {
  name: 'email-validator',
  version: '1.0.0',
  config: {
    allowInternational: true,
    requireTLD: true
  },
  install: async (api) => {
    api.addValidator('email', {
      validate: (value: string, options: EmailValidatorConfig = {}) => {
        const { allowInternational = true, requireTLD = true } = options
        
        let regex = /^[^\s@]+@[^\s@]+/
        if (requireTLD) {
          regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
        }
        
        if (!allowInternational) {
          regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
        }
        
        const isValid = regex.test(value)
        return {
          isValid,
          error: isValid ? undefined : 'Please enter a valid email address'
        }
      }
    })
  },
  uninstall: async (api) => {
    api.removeValidator('email')
  }
}

// Plugin manager implementation
class FormValidationManager implements PluginManager<FormValidationApi> {
  private plugins = new Map<string, Plugin<unknown, FormValidationApi>>()
  private validators = new Map<string, FieldValidator>()

  private api: FormValidationApi = {
    addValidator: (name, validator) => {
      this.validators.set(name, validator)
    },
    removeValidator: (name) => {
      this.validators.delete(name)
    },
    validate: (value, rules) => {
      const errors: string[] = []
      
      for (const rule of rules) {
        const validator = this.validators.get(rule)
        if (validator) {
          const result = validator.validate(value)
          if (!result.isValid && result.error) {
            errors.push(result.error)
          }
        }
      }
      
      return {
        isValid: errors.length === 0,
        errors
      }
    }
  }

  async register<TConfig>(plugin: Plugin<TConfig, FormValidationApi>): Promise<void> {
    if (this.plugins.has(plugin.name)) {
      throw new Error(`Plugin ${plugin.name} is already registered`)
    }
    
    await plugin.install(this.api)
    this.plugins.set(plugin.name, plugin as Plugin<unknown, FormValidationApi>)
  }

  async unregister(name: string): Promise<void> {
    const plugin = this.plugins.get(name)
    if (plugin && plugin.uninstall) {
      await plugin.uninstall(this.api)
    }
    this.plugins.delete(name)
  }

  getPlugin(name: string): Plugin<unknown, FormValidationApi> | undefined {
    return this.plugins.get(name)
  }

  getPlugins(): Plugin<unknown, FormValidationApi>[] {
    return Array.from(this.plugins.values())
  }

  // Expose validation API
  validate(value: any, rules: string[]): ValidationResult {
    return this.api.validate(value, rules)
  }
}

// Usage
const validationManager = new FormValidationManager()

// Register plugins
await validationManager.register(emailValidatorPlugin)

// Use validation
const emailResult = validationManager.validate('user@example.com', ['email'])
console.log(emailResult) // { isValid: true, errors: [] }

const invalidEmailResult = validationManager.validate('invalid-email', ['email'])
console.log(invalidEmailResult) // { isValid: false, errors: ['Please enter a valid email address'] }

Advanced Generic Patterns

Conditional Type Chains

Create complex conditional type logic.

// Conditional type for API responses
type ApiResponseType<T> = 
  T extends 'user' ? User :
  T extends 'users' ? User[] :
  T extends 'product' ? Product :
  T extends 'products' ? Product[] :
  T extends 'order' ? Order :
  T extends 'orders' ? Order[] :
  never

// Generic API client
interface ApiClient {
  get<T extends string>(endpoint: T): Promise<ApiResponseType<T>>
}

class TypedApiClient implements ApiClient {
  async get<T extends string>(endpoint: T): Promise<ApiResponseType<T>> {
    const response = await fetch(`/api/${endpoint}`)
    return response.json() as Promise<ApiResponseType<T>>
  }
}

// Usage with full type inference
const client = new TypedApiClient()

// TypeScript infers return type as User
const user = await client.get('user')

// TypeScript infers return type as User[]
const users = await client.get('users')

// TypeScript infers return type as Product
const product = await client.get('product')

Recursive Type Patterns

Create types that work with nested structures.

// Recursive menu structure
interface MenuItem {
  id: string
  label: string
  icon?: string
  href?: string
  children?: MenuItem[]
}

// Recursive type to flatten menu structure
type FlatMenuItem<T extends MenuItem> = {
  id: T['id']
  label: T['label']
  icon: T['icon']
  href: T['href']
  level: number
  path: string[]
} & (T['children'] extends MenuItem[] 
  ? { children: FlatMenuItem<T['children'][number]>[] }
  : { children?: never }
)

// Recursive function to flatten menu
function flattenMenu(items: MenuItem[], level = 0, path: string[] = []): FlatMenuItem<MenuItem>[] {
  return items.map(item => ({
    id: item.id,
    label: item.label,
    icon: item.icon,
    href: item.href,
    level,
    path: [...path, item.id],
    children: item.children ? flattenMenu(item.children, level + 1, [...path, item.id]) : undefined
  })) as FlatMenuItem<MenuItem>[]
}

// Tree traversal types
type TreeNode<T> = T & {
  children?: TreeNode<T>[]
}

type TreeTraversal<T> = {
  preOrder: (node: TreeNode<T>) => void
  postOrder: (node: TreeNode<T>) => void
  inOrder: (node: TreeNode<T>) => void
  breadthFirst: (node: TreeNode<T>) => void
}

function createTreeTraversal<T>(): TreeTraversal<T> {
  return {
    preOrder: (root) => {
      const visit = (node: TreeNode<T>) => {
        console.log(node)
        node.children?.forEach(visit)
      }
      visit(root)
    },
    postOrder: (root) => {
      const visit = (node: TreeNode<T>) => {
        node.children?.forEach(visit)
        console.log(node)
      }
      visit(root)
    },
    inOrder: (root) => {
      const visit = (node: TreeNode<T>) => {
        const children = node.children || []
        const mid = Math.floor(children.length / 2)
        
        children.slice(0, mid).forEach(visit)
        console.log(node)
        children.slice(mid).forEach(visit)
      }
      visit(root)
    },
    breadthFirst: (root) => {
      const queue: TreeNode<T>[] = [root]
      
      while (queue.length > 0) {
        const node = queue.shift()!
        console.log(node)
        
        if (node.children) {
          queue.push(...node.children)
        }
      }
    }
  }
}

Next Steps