Skip to content

Latest commit

 

History

History
127 lines (97 loc) · 3.5 KB

File metadata and controls

127 lines (97 loc) · 3.5 KB
paths
**/*.ts
**/*.tsx

Function Rules

Object parameters for multiple arguments

Functions with >1 parameter use an object parameter with a {FunctionName}Input type.

type FormatCurrencyInput = { value: number; currency: string; decimals?: number }
const formatCurrency = ({ value, currency, decimals }: FormatCurrencyInput) => { ... }

Exceptions: two obvious args with clear context (Math.max(a, b)), or required arg + optional config.

attempt() over try-catch

  • NEVER use try-catch blocks for error handling
  • ONLY use attempt() for: showing errors to users, executing alternative logic, or providing fallback values
  • Let errors bubble up naturally — don't wrap functions just to log
// Good — show error to user
const result = await attempt(() => saveUserData(data))
if ('data' in result) showSuccessMessage()
else showErrorToUser(result.error.message)

// Good — fallback value
const config = attempt(() => parseConfig(raw))
const finalConfig = 'data' in config ? config.data : defaultConfig

// Bad — catching just to log
try {
  await saveUserData(data)
} catch (e) {
  console.error(e)
  throw e
}

Pure functions for data transformation

Mappers, formatters, and transformers must be pure — no side effects, no mutations, no external state.

// Good — pure mapper
const mapApiTokenToToken = (apiToken: ApiToken): Token => ({
  symbol: apiToken.symbol,
  price: parseNumber(apiToken.price),
  change24h: parseNumber(apiToken.change_24h),
})

// Bad — mutates input
const mapApiTokenToToken = (apiToken: ApiToken) => {
  apiToken.price = parseFloat(apiToken.price) // mutation!
  return apiToken as unknown as Token // cast!
}

Early returns for guard clauses

Use early returns to handle edge cases at the top, keeping the happy path unindented.

// Good — guards at top, happy path flat
const processOrder = (order: Order) => {
  if (order.amount <= 0) return { error: 'Invalid amount' }
  if (!order.tokenId) return { error: 'Missing token' }

  const fee = calculateFee(order)
  const total = order.amount + fee
  return { data: { ...order, fee, total } }
}

// Bad — deeply nested
const processOrder = (order: Order) => {
  if (order.amount > 0) {
    if (order.tokenId) {
      const fee = calculateFee(order)
      // ... deep nesting continues
    }
  }
}

Naming conventions

  • Actions: verb prefix — createOrder, updatePosition, deleteToken
  • Getters: get prefix — getTokenPrice, getFormattedBalance
  • Predicates: is/has/should prefix, returns boolean — isActive, hasBalance, shouldRefetch
  • Mappers: mapXxxToYyymapApiTokenToToken, mapOrderToUiOrder
  • Formatters: format prefix — formatCurrency, formatPercent
  • Validators: validate prefix — validateOrder, validateAddress
  • Event handlers: handleXxx internally, onXxx for props

Arrow functions for everything

Use arrow function expressions, not function declarations.

// Good
const calculateFee = (amount: number) => amount * 0.001

// Bad
function calculateFee(amount: number) {
  return amount * 0.001
}

Exception: exported functions that need hoisting (rare).

Single responsibility

Each function does one thing. If a function name needs "and" to describe it, split it.

// Bad
const fetchAndFormatTokens = async () => { ... }

// Good
const fetchTokens = async () => { ... }
const formatTokens = (tokens: Token[]) => { ... }