· 3 min read
The ternary hangover
AI writes nested ternaries without feeling their weight, leaving the reviewer to reconstruct control flow from punctuation.

The patch compiles, the tests are green, and the agent's summary says it simplified the conditional. Then you open the diff, and you spend the next thirty seconds counting question marks and matching each colon to the branch that owns it.
const badge = user == null ? "Sign in" : user.suspended ? "Suspended" : user.plan === "pro" ? user.trialEndsAt > now ? "Pro trial" : "Pro" : user.trialEndsAt > now ? "Trial" : "Free";AI didn't invent the ternary; it removed the friction that used to keep the operator small. The machine types the puzzle in a second, and the decoding falls to whoever reviews the patch.
The author no longer pays#
A flat ternary is fine. disabled ? "muted" : "normal" asks one boring question and returns one of two obvious values, which is exactly the three-part shape the JavaScript reference describes. The reference also notes that the operator is right-associative, which is what makes chains like the one above legal in the first place.
Writing a nested chain by hand makes you feel its weight: you have to remember which condition you're inside, where the false branch resumes, and whether the formatter has dragged the punctuation away from the thought it belongs to. That discomfort is what used to stop me after the first ?. An agent never gets tired of colons — it can preserve the local expression, keep the diff small, satisfy the type checker, and hand you the whole decision tree as a string of question marks.
None of this is a new complaint. The ESLint rule against nested ternaries says plainly that nesting makes code harder to understand, and Prettier's history of ternary formatting reads like a case file — years of experiments, a flat style that got proposed and then rejected, and steady pressure from users who wanted the branches legible. If a formatter needs a new visual grammar for one operator, the operator has already exceeded its useful range.
badge.ts
+const badge =+ user == nullReviewer
Does the user exist?
+ ? "Sign in"+ : user.suspendedReviewer
Is the account suspended?
+ ? "Suspended"+ : user.plan === "pro"Reviewer
Is the plan Pro?
+ ? (user.trialEndsAt > now ? "Pro trial" : "Pro")Reviewer
Is the trial active?
+ : (user.trialEndsAt > now ? "Trial" : "Free");Reviewer
Which fallback applies?
One ternary, one question#
The replacement is longer because the program genuinely contains several decisions, and the compressed version was hiding all of them — along with their names — behind punctuation.
function badgeFor(user: User | null, now: Date): Badge { if (!user) return "Sign in"; if (user.suspended) return "Suspended"; const trialActive = user.trialEndsAt > now; if (user.plan === "pro") { return trialActive ? "Pro trial" : "Pro"; } return trialActive ? "Trial" : "Free";}Now you can scan the exits, set a breakpoint on suspension, name the trial rule, add a case without rebalancing a tower of colons, and ask an agent to change one branch without inviting it to rewrite the whole expression. That's what the extra lines buy.
Keep a ternary when both outcomes are short, unsurprising values and the condition carries no hidden policy. The moment a second question shows up, reach for an if, a switch, or a function with a name — and in JSX, extract the decision before the markup turns into a staircase.
Code is cheap to write now, which is a reason to hold review to a higher standard, not a lower one. Let the model save keystrokes somewhere else; you're the one who'll be rereading this branch long after the chat window is gone.