Skip to content

Latest commit

 

History

History
192 lines (153 loc) · 4.97 KB

File metadata and controls

192 lines (153 loc) · 4.97 KB
paths
**/*.ts
**/*.tsx

Pattern Matching

Never use switch/case, nested ternaries, or if/else chains. Use these patterns instead.

Decision tree — when to use what

Scenario Pattern Example
Value → value mapping Record<Key, Value> status → color, type → label
Value → logic/side effects match(value, { ... }) animal → sound function
Value → JSX branch <Match value={} ... /> status → loading/error/success
Query → JSX branch <MatchQuery value={} ... /> React Query → pending/error/success
Discriminated union dispatch matchDiscriminatedUnion() action.kind → handler
Record union dispatch matchRecordUnion() shape type → area calculator
Growing repetitive logic Config arrays filters, validators, form fields
Boolean conditions Ternary (single level only) isActive ? 'green' : 'gray'

Record — simple value mapping

Exhaustive by default when key is a union type.

const statusToColor: Record<Status, TextColor> = {
  completed: 'contrast',
  active: 'primary',
  loading: 'supporting',
}
color={statusToColor[status]}
// Bad — switch that should be a Record
switch (status) {
  case 'completed': return 'contrast'
  case 'active': return 'primary'
  case 'loading': return 'supporting'
}

match() — functional branching

When each branch involves logic, not just a value.

const sound = match(animal, {
  dog: () => 'woof',
  cat: () => 'meow',
})

Match component — JSX conditional rendering

<Match
  value={status}
  loading={() => <LoadingSpinner />}
  error={() => <ErrorMessage />}
  success={() => <Content />}
/>
// Bad — manual branching in JSX
{status === 'loading' && <LoadingSpinner />}
{status === 'error' && <ErrorMessage />}
{status === 'success' && <Content />}

MatchQuery — React Query state rendering

Mandatory for all query-dependent UI. Never write manual if (isLoading) / if (error) / if (!data) checks.

<MatchQuery
  value={tokenQuery}
  pending={() => <Skeleton />}
  error={(error) => <ErrorState error={error} />}
  success={(data) => <TokenCard token={data} />}
/>

Pre-conditions (e.g., checking if a wallet is connected before showing query UI) go before MatchQuery — they are orthogonal to query state:

// Good — pre-condition outside, query states inside
if (!walletAddress) return <EmptyState />

return (
  <MatchQuery
    value={positionQuery}
    pending={() => <Skeleton />}
    error={() => <EmptyState />}
    success={(position) => <PositionDetails position={position} />}
  />
)

For complex success rendering, extract a typed component rather than inlining 20+ lines:

// Good — extracted component with typed props
const PositionDetails = ({ position }: { position: Position }) => { ... }

// In MatchQuery
success={(position) => <PositionDetails position={position} />}
// Bad — manual query state checks
const { data, isLoading, error } = useTokenQuery()
if (isLoading) return <Skeleton />
if (error) return <ErrorState error={error} />
if (!data) return <EmptyState />
return <TokenCard token={data} />

matchDiscriminatedUnion — discriminated unions

matchDiscriminatedUnion(action, 'kind', 'payload', {
  increment: (amount) => count + amount,
  decrement: (amount) => count - amount,
  reset: () => 0,
})

matchRecordUnion — record union types

matchRecordUnion(shape, {
  circle: (radius) => Math.PI * radius * radius,
  rectangle: ({ width, height }) => width * height,
})

Config arrays — repetitive conditionals

When logic is repetitive and grows with each new case, use config arrays. Adding a case = one config line, not a new branch.

const rangeFilters = [
  { key: 'mcap', min: 'minMcap', max: 'maxMcap' },
  { key: 'volume', min: 'minVolume', max: 'maxVolume' },
  { key: 'holders', min: 'minHolders', max: 'maxHolders' },
] as const

// Validation — all filters pass
const isValid = rangeFilters.every(f => filters[f.min] <= filters[f.max])

// Reset — derive from config
const defaultFilters = Object.fromEntries(
  rangeFilters.flatMap(f => [[f.min, 0], [f.max, Infinity]])
)
// Bad — repetitive if/else that grows with each filter
if (filters.minMcap > filters.maxMcap) return false
if (filters.minVolume > filters.maxVolume) return false
if (filters.minHolders > filters.maxHolders) return false

Anti-patterns — never do these

// Never: switch/case
switch (type) {
  case 'buy': return green
  case 'sell': return red
  default: return gray
}

// Never: nested ternary
const color = type === 'buy' ? green : type === 'sell' ? red : gray

// Never: if/else chain for value mapping
if (type === 'buy') return green
else if (type === 'sell') return red
else return gray

// Always: Record
const typeToColor: Record<TradeType, Color> = {
  buy: green,
  sell: red,
}