Skip to content

Latest commit

 

History

History
168 lines (123 loc) · 4.75 KB

File metadata and controls

168 lines (123 loc) · 4.75 KB
paths
**/*.ts
**/*.tsx

TypeScript Rules

type over interface

  • Use type for all object type definitions
  • Only use interface for contracts implemented by classes
// Good
type Position = { size: number; entryPrice: number }

// Bad
interface Position { size: number; entryPrice: number }

No type assertions (as) — zero tolerance

  • Never use as or angle-bracket casts
  • No as unknown as X escape hatches
  • Use type narrowing: type guards, predicate functions, discriminated unions
  • If the type system can't express it, redesign the API — don't cast
// Bad
const token = response.data as Token
const el = event.target as HTMLInputElement

// Good — type guard
const isToken = (data: unknown): data is Token =>
  typeof data === 'object' && data !== null && 'symbol' in data

// Good — discriminated union
type Result = { kind: 'success'; data: Token } | { kind: 'error'; message: string }

Never use any

  • Use unknown when the type is genuinely unknown, then narrow
  • Use generics when the type should be preserved but is flexible
  • Use Record<string, unknown> instead of Record<string, any>
// Bad
const parse = (data: any) => data.value

// Good
const parse = (data: unknown) => {
  if (isToken(data)) return data.value
  throw new Error('Invalid token data')
}

Derive union types from const arrays

Single source of truth — never duplicate between runtime and type level.

const sortableColumns = ['size', 'creationTime'] as const
type SortableColumn = (typeof sortableColumns)[number]

// Bad — duplicate source of truth
type SortableColumn = 'size' | 'creationTime'
const sortableColumns = ['size', 'creationTime']

Use satisfies for type-safe literals

Preserves narrow types while validating against a wider type.

// Good — narrow type preserved, still validates against Record
const statusToColor = {
  completed: 'contrast',
  active: 'primary',
} satisfies Record<Status, TextColor>
// typeof statusToColor.completed is 'contrast', not TextColor

// Bad — widens the type
const statusToColor: Record<Status, TextColor> = {
  completed: 'contrast',
  active: 'primary',
}
// typeof statusToColor.completed is TextColor

Use satisfies when you need both the narrow literal types AND the constraint validation. Use explicit annotation when the wider type is intentional (e.g., when the Record will be indexed dynamically).

Trust types — no unnecessary fallbacks

  • When a type is non-optional, use it directly
  • No optional chaining (?.) or fallback values (?? '') on guaranteed-present values
  • No ! non-null assertion — if it could be null, the type should reflect it
// Bad — position.size is number, not number | undefined
const displaySize = position.size ?? 0
const name = user?.name ?? 'Unknown' // when user is not optional

// Good
const displaySize = position.size
const name = user.name

ensurePresent for required runtime values

const apiKey = ensurePresent(process.env.API_KEY, 'API_KEY')
const selectedToken = ensurePresent(tokens.find(t => t.id === id), 'selected token')

Use readonly for immutable data

  • Mark arrays and objects as readonly when they shouldn't be mutated
  • Prefer readonly tuple types for fixed-length arrays
// Good
type Config = {
  readonly endpoints: readonly string[]
  readonly retryCount: number
}

// Good — const arrays are already readonly via `as const`
const chains = ['evm', 'solana', 'cosmos'] as const

Generic constraints — keep them minimal

  • Constrain generics only to what the function actually needs
  • Prefer extends over intersection when constraining
// Good — minimal constraint
const getLabel = <T extends { label: string }>(item: T) => item.label

// Bad — over-constrained, requires full Token type
const getLabel = <T extends Token>(item: T) => item.label

Utility types — derive, don't duplicate

Use Pick, Omit, Partial, Required to derive types from existing ones.

// Good — derived from existing type
type TokenSummary = Pick<Token, 'symbol' | 'price' | 'change24h'>
type CreateOrderInput = Omit<Order, 'id' | 'createdAt'>

// Bad — manually duplicated fields
type TokenSummary = { symbol: string; price: number; change24h: number }

No phantom API props

  • Never cast API types to pretend optional properties exist
  • If a field is needed but missing from the schema, create a derived value in a mapper

core.ts vs types.ts

  • core.ts — runtime values + derived types (const arrays, Records, domain logic)
  • types.ts — pure type definitions only (zero runtime code)
  • If it has a const array, a Record mapping, or any runtime value → core.ts