High-level constraints that govern all TypeScript in the monorepo. These rules are enforced by OXLint and OXFmt — code that violates them will not pass CI. The goal is a strict functional style: pure, immutable, declarative, with side effects pushed to the edges.
All bindings must be const. No reassignment, no mutation. Mutable state inside closures (factory internals) is the one accepted exception.
const timeout = 5000
const scripts = config.scripts.filter(isEnabled)let timeout = 5000
timeout = 10000
let scripts: Script[] = []
scripts.push(newScript)Use map, filter, reduce, flatMap, and es-toolkit utilities instead of for, while, do...while, for...in, or for...of.
const names = scripts.map((s) => s.name)
const enabled = scripts.filter((s) => s.enabled)
const total = items.reduce((sum, item) => sum + item.count, 0)const names: string[] = []
for (const script of scripts) {
names.push(script.name)
}Use plain objects, closures, and factory functions. Classes are permitted only when wrapping an external SDK that requires instantiation.
| Anti-pattern | Use Instead |
|---|---|
| Utility classes | Module with functions |
| Static method collections | Module with functions |
| Data containers | Plain objects / interfaces |
| Singletons | Module-level constants |
export function capitalize(s: string) {
return s.charAt(0).toUpperCase() + s.slice(1)
}class StringUtils {
static capitalize(s: string) {
return s.charAt(0).toUpperCase() + s.slice(1)
}
}Never reference this. Factory closures and plain functions eliminate the need.
Return errors as values using the Result tuple type. No throw statements or throw expressions.
function parseConfig(raw: string): Result<Config, ParseError> {
if (!raw) {
return [{ type: 'parse_error', message: 'Empty input' }, null]
}
return [null, JSON.parse(raw)]
}function parseConfig(raw: string): Config {
if (!raw) {
throw new Error('Empty input')
}
return JSON.parse(raw)
}Do not mutate objects or arrays after creation. Return new values from every transformation.
function addScript(scripts: readonly Script[], script: Script): readonly Script[] {
return [...scripts, script]
}function addScript(scripts: Script[], script: Script) {
scripts.push(script)
}Use if/else or match expressions. Ternaries are banned by the linter.
if (isVerbose) {
log.info(details)
} else {
log.info(summary)
}const message = isVerbose ? details : summaryUse unknown, generics, or proper types. Narrow with type guards when needed.
function parse(data: unknown): Result<Config, ParseError> {
if (!isPlainObject(data)) {
return [{ type: 'parse_error', message: 'Expected object' }, null]
}
return [null, validateConfig(data)]
}function parse(data: any): Config {
return data
}When passing a named function to a higher-order function, pass it directly instead of wrapping in an arrow.
const valid = scripts.filter(isEnabled)
const names = items.map(getName)const valid = scripts.filter((s) => isEnabled(s))
const names = items.map((item) => getName(item))Use ES module syntax with verbatimModuleSyntax. Use import type for type-only imports. Prefer the node: protocol for Node.js builtins.
import type { Config } from './types'
import { readFile } from 'node:fs/promises'
import { loadConfig } from './lib/config'const fs = require('fs')
import { Config } from './types' // should use import type
import { readFile } from 'fs' // should use node: protocolDo not use immediately invoked function expressions. Extract the logic into a named function and call it explicitly. This applies to both sync and async IIFEs.
/**
* @private
*/
async function execute(options: Options): Promise<void> {
// ...
}
function start(options: Options): void {
void execute(options)
}function start(options: Options): void {
void (async () => {
// ...
})()
}Organize imports into three groups separated by blank lines, sorted alphabetically within each group. Use top-level import type statements — do not use inline type specifiers.
- Node builtins —
node:protocol imports - External packages — third-party dependencies
- Internal imports — project-relative paths, ordered farthest-to-closest (
../before./), then alphabetically within the same depth
import { readdir } from 'node:fs/promises'
import { basename, resolve } from 'node:path'
import { isPlainObject } from 'es-toolkit'
import { match } from 'ts-pattern'
import type { Command } from '../types.js'
import { createLogger } from '../lib/logger.js'
import { registerCommandArgs } from './args.js'
import type { ResolvedRef } from './register.js'import { match } from 'ts-pattern'
import { readdir } from 'node:fs/promises' // node: should be first
import { registerCommandArgs } from './args.js'
import type { Command } from '../types.js' // ../ should come before ./
import { isPlainObject } from 'es-toolkit'
import { createLogger, type Logger } from '../lib/logger.js' // no inline type specifiersOrganize each source file in this order:
- Imports — ordered per import rules above
- Module-level constants —
constbindings used throughout the file - Exported functions — the public API, each with full JSDoc
- Section separator —
// --------------------------------------------------------------------------- - Private helpers — non-exported functions, each with JSDoc including
@private
Exported functions appear first so readers see the public API without scrolling. Private helpers are implementation details pushed to the bottom. Function declarations are hoisted, so calling order does not matter.
import type { Config } from '../types.js'
const DEFAULT_NAME = 'untitled'
/**
* Load and validate a configuration file.
*
* @param path - Absolute path to the config file.
* @returns The validated configuration record.
*/
export function loadConfig(path: string): Config {
const raw = readRawConfig(path)
return validateConfig(raw)
}
// ---------------------------------------------------------------------------
// Private
// ---------------------------------------------------------------------------
/**
* Read the raw config file from disk.
*
* @private
* @param path - Absolute path to read.
* @returns The raw config string.
*/
function readRawConfig(path: string): string {
// ...
}
/**
* Validate a raw config string against the schema.
*
* @private
* @param raw - Unvalidated config string.
* @returns A validated Config object.
*/
function validateConfig(raw: string): Config {
// ...
}// Private helper defined before exports — reader must scroll to find the API
function readRawConfig(path: string): string {
/* ... */
}
function validateConfig(raw: string): Config {
/* ... */
}
export function loadConfig(path: string): Config {
const raw = readRawConfig(path)
return validateConfig(raw)
}- Design Patterns -- Factories, pipelines, composition
- Errors -- Result type for error handling
- State -- Immutable state management
- Functions -- Pure functions and composition