· 3 min read

The ternary hangover

AI writes nested ternaries without feeling their weight, leaving the reviewer to reconstruct control flow from punctuation.

A dense field of railway switches and crisscrossing tracks in front of Utrecht Centraal station, under a web of overhead wires.
KlausMiniwolf, CC BY-SA 4.0

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.

Generated patchts
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 == null

Reviewer

Does the user exist?

+    ? "Sign in"
+    : user.suspended

Reviewer

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?

The patch adds one expression; the review has to ask five questions.

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.

Reviewed versionts
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.