| paths | ||
|---|---|---|
|
- Use
typefor all object type definitions - Only use
interfacefor contracts implemented by classes
// Good
type Position = { size: number; entryPrice: number }
// Bad
interface Position { size: number; entryPrice: number }- Never use
asor angle-bracket casts - No
as unknown as Xescape 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 }- Use
unknownwhen the type is genuinely unknown, then narrow - Use generics when the type should be preserved but is flexible
- Use
Record<string, unknown>instead ofRecord<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')
}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']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 TextColorUse 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).
- 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.nameconst apiKey = ensurePresent(process.env.API_KEY, 'API_KEY')
const selectedToken = ensurePresent(tokens.find(t => t.id === id), 'selected token')- Mark arrays and objects as
readonlywhen they shouldn't be mutated - Prefer
readonlytuple 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- Constrain generics only to what the function actually needs
- Prefer
extendsover 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.labelUse 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 }- 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— 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