| paths | ||
|---|---|---|
|
Never use switch/case, nested ternaries, or if/else chains. Use these patterns instead.
| 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' |
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'
}When each branch involves logic, not just a value.
const sound = match(animal, {
dog: () => 'woof',
cat: () => 'meow',
})<Match
value={status}
loading={() => <LoadingSpinner />}
error={() => <ErrorMessage />}
success={() => <Content />}
/>// Bad — manual branching in JSX
{status === 'loading' && <LoadingSpinner />}
{status === 'error' && <ErrorMessage />}
{status === 'success' && <Content />}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(action, 'kind', 'payload', {
increment: (amount) => count + amount,
decrement: (amount) => count - amount,
reset: () => 0,
})matchRecordUnion(shape, {
circle: (radius) => Math.PI * radius * radius,
rectangle: ({ width, height }) => width * height,
})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// 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,
}