Skip to content

Code Style

The companion to Project Structure, which covers where files go. This page covers what goes inside them.

The full spec lives at references/code-style.md.

Same reason as the structure rules: AI-generated code has no value if a human can’t read it. Code is read far more often than written, and by people with less context than the author had.

The same tiebreaker applies. Where a rule and readability conflict, readability wins — and the deviation gets argued at the build gate, not taken quietly. The deviation protocol covers both references.

strict: true is the floor, not the goal. These four aren’t included in strict, and each catches a real class of bug:

{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true, // arr[0] is T | undefined — it really is
"exactOptionalPropertyTypes": true, // `{a?: string}` ≠ `{a: string | undefined}`
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}

New projects get this in the initial tsconfig.json. Existing projects that don’t have it get the gap reported, not silently flipped on.

ConstructRule
anyBanned. Use unknown and narrow. any doesn’t disable one check — it disables all of them, silently, downstream
as assertionsAvoid. A type guard proves the claim; an assertion only asserts it. Fine for as const and genuinely un-typable boundaries
! non-nullAvoid. If it can’t be null, the type should say so. If it can, handle it
@ts-ignoreNever — use @ts-expect-error. It fails the build once the underlying issue is fixed, so it can’t rot silently

Each of these is counted by /mx:ratchet as a suppression, so adding one shows up by file:line in the diff against trunk. That’s the intended cost: possible, never free, never silent.

The hardest rules to enforce and the most important. Overcomplicated code is the single biggest readability tax in AI-generated work — generating an abstraction is as cheap as generating the thing itself, so the friction that normally stops a human from over-building isn’t there.

Don’t build what isn’t needed yet. No configuration option with one caller. No interface with one implementation. No generic parameter with one concrete type. No factory that constructs one kind of thing. If the second case never arrives, the abstraction was pure cost.

Rule of three. Wait for the third occurrence before extracting a shared abstraction. Two similar things are often coincidence; the third tells you the shape. Duplication is cheaper to fix than the wrong abstraction — one is a find-and-replace, the other is a refactor across every call site.

An abstraction with exactly one caller is a candidate for inlining. Not automatically wrong — extracting for a name, or to make a long function scannable, is legitimate — but it should survive the question being asked.

Delete, don’t comment out. Dead code behind a comment is a question every future reader has to answer. Git remembers.

No cleverness. If a line needs a comment explaining how it works, rewrite the line. Comments explain why.

Guard clauses over nesting. Handle exceptional cases first and return early, so the happy path stays at the left margin and reads top to bottom.

// Avoid — the actual work is buried three levels deep
function processOrder(order: Order) {
if (order.items.length > 0) {
if (order.customer.isVerified) {
if (order.payment.status === 'authorized') {
return submitOrder(order)
}
}
}
}
// Prefer — every precondition stated once, then the work
function processOrder(order: Order) {
if (order.items.length === 0) return
if (!order.customer.isVerified) return
if (order.payment.status !== 'authorized') return
return submitOrder(order)
}

Name your conditions. A compound boolean is unreadable inline and self-documenting once it has a name.

// Avoid
if (user.age >= 18 && user.country === 'US' && !user.restrictions.includes('trading')) { … }
// Prefer
const isEligibleTrader =
user.age >= 18 && user.country === 'US' && !user.restrictions.includes('trading')
if (isEligibleTrader) { … }
RuleLimitESLint
Nesting depth3max-depth
Cyclomatic complexity10complexity
Nested ternaries0no-nested-ternary

A function that can’t meet these is telling you it does more than one thing. Split it — don’t reformat it into compliance.

Names spell things out. No single letters, no invented abbreviations.

AvoidPrefer
const u = getUser()const user = getUser()
const usrCntconst userCount
const btnLblconst buttonLabel
const res, const respconst response
const tmp, const data, const infoa name that says what it holds
function calc()function calculateTotal()

The only accepted single letter is _ for an intentionally unused binding. If you want i, a for...of or .map() usually removes the need for an index entirely.

Established acronyms stayid, url, api, http, css, plus your domain’s real vocabulary. The test is whether a new team member recognizes it on day one without asking. sku passes. usrCnt doesn’t.

Name length scales with scope. A binding used two lines later can be short. One exported from a module is read by people who can’t see its definition and has to carry its meaning alone.

KindShapeExample
FunctionVerb phrasecalculateTotal, fetchInvoice
VariableNoun phraseinvoiceTotal, pendingItems
BooleanPredicateisVerified, hasPermission, canCheckout, shouldRetry
CollectionPluralorders — not orderList or orderArray
Handlerhandle + eventhandleSubmit, handleRetryClick
ConstantSCREAMING_SNAKE_CASEMAX_RETRY_ATTEMPTS

Avoid noise-word suffixes — userData, orderInfo, configObject, itemManager. If removing the suffix loses nothing, it was never carrying anything.

An honest split between what CI catches and what needs a reader.

RuleMechanism
Strict TypeScripttsconfig.json — CI gate
No any@typescript-eslint/no-explicit-any — CI gate
No non-null assertion@typescript-eslint/no-non-null-assertion — CI gate
@ts-expect-error over @ts-ignore@typescript-eslint/ban-ts-comment — CI gate
Nesting, complexity, nested ternariesmax-depth, complexity, no-nested-ternary — CI gate
Unused codenoUnusedLocals / noUnusedParameters — CI gate
Suppression count vs trunk/mx:ratchet — CI gate
Naming qualityReview — mx-code-reviewer
Premature abstraction, rule of threeReview — mx-code-simplifier
Guard clauses, named conditionsReview — mx-code-simplifier

The bottom three can’t be linted, and pretending otherwise would be worse than admitting it. They’re also the ones AI-generated code gets wrong most often, which is why they’re surfaced at the build gate rather than left to chance.