Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposals for nested ternary formatting #9561

Open
rattrayalex opened this issue Nov 1, 2020 · 77 comments
Open

Proposals for nested ternary formatting #9561

rattrayalex opened this issue Nov 1, 2020 · 77 comments
Labels
lang:javascript Issues affecting JS status:needs discussion Issues needing discussion and a decision to be made before action can be taken

Comments

@rattrayalex
Copy link
Collaborator

rattrayalex commented Nov 1, 2020

Background

In #737, an alternative format was proposed for nested ternaries, on the grounds that indenting every level of a nested ternary looked bad, especially for long nested ternaries.

However, there were concerns from core maintainer @vjeux that the proposed format would not be compatible with the way we currently do ternaries, and would present problems with long lines.

The community suggested several alternatives, and the one chosen was the simplest – simply flatten them: #5039. Unfortunately, this did not work well in many cases, and will likely be reverted, which brings us back to the drawing board.

It would be nice to come up with a solution for the cases where indented nested ternaries are objectionable prior to merging the revert, to minimize churn in the community. However, I think we should timebox this search to a week or two.

In #9552, I made one attempt at improving nested/chained ternary formatting, but it wasn't great, so I decided to scour proposed solutions and found two candidates that I think could be made to work well.

Note that we may wish to apply these proposals in only certain circumstance, at least at first; more on that below.

Goals

I'd like to find a solution that:

  1. Scales well to long ternary chains (unlike indents, as in Add indentation back to nested ternaries #9559).
  2. Transitions well from "single ternary" to "nested/chained ternary".
  3. Transitions well from "non-JSX" to "JSX" (since I think we really like our current behavior for that use-case).
  4. Works well for TypeScript Conditionals.
  5. Works well for simple nested if-else/case chains, as well as more complex situations.
  6. Causes minimal churn in existing codebases, especially those not using nested ternaries.
  7. Is concise.
  8. Clearly highlights the logical structure of the expression.

I don't think something that fully satisfies all of the above constraints, so tradeoffs may be required.

Option A: case-style

const message =
  i % 3 === 0 && i % 5 === 0 ? "fizzbuzz" : 
  i % 3 === 0 ? "fizz" : 
  i % 5 === 0 ? "buzz" :
  String(i);

const paymentMessage =
  state == "success" ? "Payment completed successfully" :
  state == "processing" ? "Payment processing" :
  state == "invalid_cvc" ? (
    "There was an issue with your CVC number, and here is some extra info too." 
  ) :
  state == "invalid_expiry" ? "Expiry must be sometime in the past." :
  "There was an issue with the payment. Please contact support.";

// we would probably just leave non-chained ternaries alone. 
const simple = children && !isEmptyChildren(children) 
  ? children 
  : null
// if we didn't, they'd look like this:
const simple = 
  children && !isEmptyChildren(children) ? children :
  null

const simpleChained =
  children && !isEmptyChildren(children) ? children : 
  component && props.match ? React.createElement(component, props) :
  render && props.match ? render(props) : 
  null

const complexNested =
  children && !isEmptyChildren(children) ? children : 
  props.match ? (
    component ? React.createElement(component, props) : 
    render ? render(props) : 
    null
  ) : 
  null;

type TypeName<T> =
  T extends string ? "string" :
  T extends number ? "number" :
  T extends boolean ? "boolean" :
  T extends undefined ? "undefined" :
  T extends Function ? "function" :
  "object";

type Unpacked<T> =
  T extends (infer U)[] ? U :
  T extends (...args: any[]) => infer U ? (
    SomeVeryLongNameOfSomeKind<U>
  ) :
  T extends Promise<infer U> ? U :
  T;

This is the format that was originally proposed in #737.

case-style pros

  1. Very concise.
  2. Looks nice in simple cases (eg; fizzbuzz, simple TS conditional types).
  3. When a consequent or alternate is long, breaks nicely with parens (basically same as JSX-style).
  4. Suits most goal criteria.

case-style cons

  1. When the conditional is complex, can be hard to scan and find the ?, obscuring the program structure.
  2. Looks totally different from a non-chained ternary.
  3. Not widely used in the wild; some users would be surprised and bothered by the change. It's quite different.
  4. Could cause substantial (unuseful) git diffs when a ternary is added or removed to a chain.
  5. If we decide to do "all-JSX-style" if any branch breaks lines, would also cause git diffs to the entire ternary tree when a single condition becomes large.

Incremental adoption

Since this proposal is a little bolder and risks being disruptive, we should ease into it.

I think we should merge #9559, but carve out one or two special cases where case-style clearly makes sense, namely:

  1. TS Conditional Types.
  2. Ternary chains with 3 or more cases (and thus more likely to be intended as a "case expression" rather than just an if-else), as suggested by @j-f1 here.

If these were well-received, we could consider adopting the style more broadly at a later time.
If not, the blast radius will be much more limited (both of these cases are likely fairly rare) and we can revert without causing huge amounts of churn.

Option B: if-else-style

This is a compromise between fully flattened (#5039) and fully indented (#9559) that feels more familiar than the solution above:

const message =
  i % 3 === 0 && i % 5 === 0 
    ? "fizzbuzz" 
  : i % 3 === 0 
    ? "fizz" 
  : i % 5 === 0 
    ? "buzz" 
    : String(i);

const paymentMessage =
  state == "success"
    ? "Payment completed successfully"
  : state == "processing"
    ? "Payment processing"
  : state == "invalid_cvc"
    ? "There was an issue with your CVC number"
  : state == "invalid_expiry"
    ? "Expiry must be sometime in the past."
    : "There was an issue with the payment.  Please contact support.";

const simple = 
  children && !isEmptyChildren(children) 
    ? children
    : null

const simpleChained =
  children && !isEmptyChildren(children) 
    ? children
  : component && props.match
    ? React.createElement(component, props) 
  : render && props.match
    ? render(props)
    : null

const complexNested =
  children && !isEmptyChildren(children)
    ? children
  : props.match
    ? (component
        ? React.createElement(component, props)
      : render
        ? render(props)
        : null)
    : null;

type TypeName<T> =
  T extends string
    ? "string"
  : T extends number
    ? "number"
  : T extends boolean
    ? "boolean"
  : T extends undefined
    ? "undefined"
  : T extends Function
    ? "function"
    : "object";

type Unpacked<T> =
  T extends (infer U)[]
    ? U
  : T extends (...args: any[]) => infer U
    ? SomeVeryLongNameOfSomeKind<U>
  : T extends Promise<infer U>
    ? U
    : T;

I first saw this approach outlined here by @JAForbes.

if-else pros

  1. Clear logical structure; reads very much like an if-else chain.
  2. Very familiar/similar to our current way of doing ternaries, single or nested.
  3. Suits most criteria above.

if-else cons

  1. Subjectively, does not look good for TS Conditional Types (eager for TS community feedback on this).
  2. Not concise, especially for simple "case expressions" like paymentMessage above.
  3. Complex consequents (eg; complex nested, above) are a little difficult to read.

Personally, I think this option is strictly superior to both the fully-flat and fully-nested behaviors. However, I'm concerned that it still leaves TS in the lurch.

My take

EDIT: upon further exploration, I think Option A gets too special-case-y and will result in strange git diffs, and big/strange differences in code style that will frequently surprise and confuse users. Unless we were to shift to using it for ternaries everywhere, I think we should not adopt Option A.

Personally, I actually quite like Option B2, proposed by @roryokane below, with a prior implementation by @zeorin in #4767, and I think both Option B and Option B2 warrant further exploration.

@rattrayalex
Copy link
Collaborator Author

Option A

@rattrayalex
Copy link
Collaborator Author

Option B

@troglotit
Copy link

To "my taste", they're both inferior to both current "flat", and previous "binary-tree" options.

@mike-marcacci
Copy link

mike-marcacci commented Nov 1, 2020

I prefer option B over option A, but still prefer an unimproved reversion to nested types per #9559, as having a concise (or even attractive) representation is far far less of a priority for me than regaining the ability to quickly glean the logical relationships between paths even when only looking at a subset of a large ternary... and IMO nothing even approaches the nested format on that front.

As an avid user of TS conditional types, I would add a vote for supporting the nested structure there as well. It's not uncommon for me to end up in the "complex" case, where sub conditions exist in both branches at multiple depths.

But regardless of which way this goes, I just want to thank you @rattrayalex for putting in all this thought and work on the different options, and including solid examples for each proposal 🙌

(Edited for clarity)

@rattrayalex
Copy link
Collaborator Author

rattrayalex commented Nov 1, 2020

😄 thanks Mike! I really appreciate the kind words.

Just to be clear, you're saying that you prefer the behavior in #9559 to either of the behaviors in this proposal for TS conditional types?

EDIT: And by the way, if you'd be willing to find a particularly complex example in your codebase, I'd much appreciate it! No worries if you're not able to.

@DScheglov
Copy link

I vote for "Option A".

And actually we can create "Option C":

const message =
  i % 3 === 0 && i % 5 === 0 
    ? "fizzbuzz"  : 
  i % 3 === 0 
    ? "fizz" : 
  i % 5 === 0 
    ? "buzz"  : 
  String(i);

It is something like compromise between Option A and Option B.

@mike-marcacci
Copy link

Hi @rattrayalex! Regarding an example, much of our public code has been refactored over the past year or so to remove these kind of ternaries due to readability issues with the formatting. However, here is a public file of ours with various ternaries of different depths and complexities impeded to different degrees by the current rules.

In case it's helpful, here are some more examples from non-public code in a codebase of mine that uses a patch to restore the original nested format:

const args = [
  context.requestId,
  context.sessionId,
  context.userId,
  ...columnFields.map(([field, { encode, default: d }]) =>
    encode
      ? encode(
          typeof data[field] === "undefined"
            ? typeof d === "undefined"
              ? null
              : d
            : data[field]
        )
      : typeof data[field] === "undefined"
        ? typeof d === "undefined"
          ? null
          : d
        : data[field]
  )
];

// --------------------------------------------------

// get the field
return definition.encode
  ? definition.encode(
      typeof row[field] === "undefined"
        ? typeof definition.default === "undefined"
          ? null
          : definition.default
        : row[field]
    )
  : typeof row[field] === "undefined"
    ? typeof definition.default === "undefined"
      ? null
      : definition.default
    : row[field];

I think that my greatest frustration with the current behavior, and my concern with some of these other proposals, is that a reader has to expect different formatting based on the logic itself. This means you need to understand the logic before the formatting is helpful, which defeats the whole point of formatting.

Given this and my stance that the nested format is the only useful one for the complex case, I do think my real preference is for restoration of the indented form, per #9559. I do acknowledge the issue of "scrunching lines" with deep indentation, but this is an acceptable tradeoff in my use cases.

@mike-marcacci
Copy link

mike-marcacci commented Nov 1, 2020

Also, regarding your request for examples of TypeScript conditionals, I'm not sure what your timeline is on this ternary issue, but this major initiative in graphql-js is on the docket immediately following or part of the codebase conversion to TS. This feature will require extensive use of conditionals - I suspect it will require heavier use than any other codebase I've worked with or read.

The feature essentially uses the GraphQL object configurations to check or infer resolver call signatures, and given that ternaries are the only mechanism for describing conditional types, there is a good chance that this will find the limits of any formatting strategy 🙃

@jessejanderson
Copy link

I slightly prefer Option B but either Option A or Option B would be a vast improvement over the previous methods (flat or nested), "to my taste". 😉

Thanks for taking the time to write this all up.

@rattrayalex
Copy link
Collaborator Author

rattrayalex commented Nov 1, 2020

Thanks for the real-world examples, Mike! I decided to peel off the last one and see how they compare.

Turns out Option A would be the exact same as indented, since this isn't a flat-nested ternary chain, and there's only 2 cases. However, I illustrated what it would look like if the minimum was a ternary chain length of 2 instead of 3.

Examples with the code as written:
// indented:
const result = definition.encode
  ? definition.encode(
      typeof row[field] === "undefined"
        ? typeof definition.default === "undefined"
          ? null
          : definition.default
        : row[field]
    )
  : typeof row[field] === "undefined"
    ? typeof definition.default === "undefined"
      ? null
      : definition.default
    : row[field];

// option A – actually the same as above(!!), since the longest ternary chain is 3.
// If we were to adjust it to a minimum length of 2, this is how it'd look:
const result = 
  definition.encode ? (
    definition.encode(
      typeof row[field] === "undefined" 
        ? typeof definition.default === "undefined" 
          ? null 
          : definition.default
        : row[field]
    )
  ) : typeof row[field] === "undefined" ? (
    typeof definition.default === "undefined" 
      ? null 
      : definition.default
  ) : 
  row[field];

// option B:
const result = 
  definition.encode
    ? definition.encode(
        typeof row[field] === "undefined"
          ? (typeof definition.default === "undefined"
            ? null
            : definition.default)
          : row[field]
      )
  : typeof row[field] === "undefined"
    ? (typeof definition.default === "undefined"
      ? null
      : definition.default)
    : row[field];

// option B2 (see below):
const result = 
  definition.encode ?
    definition.encode(
      typeof row[field] === "undefined" ?
        typeof definition.default === "undefined" ?
          null :
          definition.default :
        row[field]
    ) :
  typeof row[field] === "undefined" ?
    typeof definition.default === "undefined" ?
      null :
      definition.default :
    row[field];

Ultimately, I thought all 3 looked pretty similar in this case.

However, I was also curious what it would look like if the undefined-checks were flipped, making the ternaries more of a flat chain:

Examples with the ternaries flattened into a chain:
// indented:
const result = definition.encode
  ? definition.encode(
      typeof row[field] !== "undefined"
        ? row[field]
        : typeof definition.default !== "undefined"
          ? definition.default
          : null
    )
  : typeof row[field] !== "undefined"
    ? row[field]
    : typeof definition.default !== "undefined"
      ? definition.default
      : null

// option A:
// (note that one of the ternary chains is length 3, and the inner one length 2, so they look different. Don't love it.)
const result =
  definition.encode ? (
    definition.encode(
      typeof row[field] !== "undefined"
        ? row[field]
        : typeof definition.default !== "undefined"
          ? definition.default
          : null
    )
  ) :
  typeof row[field] !== "undefined" ? row[field] :
  typeof definition.default !== "undefined" ? definition.default :
  null

// option B:
const result =
  definition.encode
    ? definition.encode(
        typeof row[field] !== "undefined"
          ? row[field]
        : typeof definition.default !== "undefined"
            ? definition.default
            : null
      )
  : typeof row[field] !== "undefined"
    ? row[field]
  : typeof definition.default !== "undefined"
    ? definition.default
    : null

// option B2 (see below):
const result =
  definition.encode ?
    definition.encode(
      typeof row[field] !== "undefined" ?
        row[field] :
      typeof definition.default !== "undefined" ?
        definition.default :
        null
    ) :
  typeof row[field] !== "undefined" ?
    row[field] :
  typeof definition.default !== "undefined" ?
    definition.default :
    null;

Honestly, this gave me a lot of pause about Option A with the rule around only entering it with a chain of 3 or more. It just makes ternaries look inconsistent and even more confused – it's readable enough, but your eyes don't learn where to look to figure out the structure of a complex expression involving ternaries.

This makes me lean personally towards using Option A for TS Conditional Types only, which it turns out is already being worked on in #7948 (just saw for the first time). Of course, if the format turns out to be popular, we could consider adopting it in ternaries more widely in the future.

I'm not sold on this – I think Option B performed nicely in the above examples, and Option A was mostly only confusing when situated next to shorter ternaries – but more hesitant about Option A than I was before.

EDIT: I have added Option B2, which I think actually looks the best in these cases.

@roryokane
Copy link
Contributor

roryokane commented Nov 2, 2020

I prefer Option B to Option A. However, I would like to propose a modification of Option B that I like more than either of those. The modification is to place the ? and : punctuation at the end of lines, rather than the beginning of lines. Let’s call it Option B2:

Option B2: if-else-style, trailing punctuation

const message =
  i % 3 === 0 && i % 5 === 0 ?
    "fizzbuzz" :
  i % 3 === 0 ?
    "fizz" :
  i % 5 === 0 ?
    "buzz" :
    String(i);

const paymentMessage =
  state == "success" ?
    "Payment completed successfully" :
  state == "processing" ?
    "Payment processing" :
  state == "invalid_cvc" ?
    "There was an issue with your CVC number" :
  state == "invalid_expiry" ?
    "Expiry must be sometime in the past." :
    "There was an issue with the payment.  Please contact support.";

const simple =
  children && !isEmptyChildren(children) ?
    children :
    null

const simpleChained =
  children && !isEmptyChildren(children) ?
    children :
  component && props.match ?
    React.createElement(component, props) :
  render && props.match ?
    render(props) :
    null

const complexNested =
  children && !isEmptyChildren(children) ?
    children :
  props.match ?
    component ?
      React.createElement(component, props) :
    render ?
      render(props) :
      null :
    null;

type TypeName<T> =
  T extends string ?
    "string" :
  T extends number ?
    "number" :
  T extends boolean ?
    "boolean" :
  T extends undefined ?
    "undefined" :
  T extends Function ?
    "function" :
    "object";

type Unpacked<T> =
  T extends (infer U)[] ?
    U :
  T extends (...args: any[]) => infer U ?
    SomeVeryLongNameOfSomeKind<U> :
  T extends Promise<infer U> ?
    U :
    T;

This was first proposed by @0x24a537r9 in this comment. I never saw any subsequent comments discuss it.

pros compared to Option B

  1. It’s easier to tell which lines are conditions and which lines are values: lines ending with ? are conditions, and lines ending with : or ; are values. Having conditions end with question marks matches English sentence structure.

    In contrast, Option B confuses me whenever I see it because its lines beginning with ? are values and its lines beginning with : can be either conditions or values. That is different from the usual meaning of those characters.

  2. Indentation is a little easier to scan because ternary punctuation, which is visually light, is kept away from the beginning of the line. Option A shares this pro.

  3. It is more consistent with existing Prettier rules that place the binary logical operators &&, ||, and ?? at the ends of lines. (As it puts both punctuation characters at the ends of lines, it’s internally consistent as well, the same as Option B.)

cons compared to Option B

  1. () parens can look confusing in nested ternaries, which is why I removed them from the complexNested example. Their absence means the reader has only one signal to tell them of the nesting level, indentation. In comparison, Option B has two signals, both indentation and parens.

    • In what way do parens look confusing with this style? See how complexNested would look with parens:

      const complexNested =
        children && !isEmptyChildren(children) ?
          children :
        props.match ?
          (component ?
            React.createElement(component, props) :
          render ?
            render(props) :
            null) :
          null;

      I think in this version, it looks weird that render is indented further out than component, as if it were out of the ( paren group. Thus, in my proposal I removed the parens.

I am aware that this Option B2 doesn’t fix all the cons of Option B as compared to Option A, but I think it is an improvement overall and should be the main contender against Option A.

@zeorin
Copy link

zeorin commented Nov 2, 2020

This was first proposed by @0x24a537r9 in this comment. I never saw any subsequent comments discuss it.

I actually made a PR based on that comment: #4767, but it was never merged.

@troglotit
Copy link

Can we see how Option A will work when nested in truthy branch? All I see is perfect switch-case like scenarios for that option, which doesn't really help.

@rattrayalex
Copy link
Collaborator Author

Thanks @roryokane for reviving @0x24a537r9 idea (and @zeorin for putting up the earlier PR). I'll admit I had overlooked this approach, and seeing the examples and advantages spelled out by @roryokane is quite compelling to me (thanks for doing the work there!!).

I particularly like that it brings ternaries into consistency with boolean operators, and the simple heuristic that "a ? at the end means it's a condition", which is both natural and clear.

Honestly, looking at some examples, Option B2 looks the most like "prettier-formatted" code, probably because of the operator-at-end-of-line consistency.

I agree with the removal of parens in this case, for the reasons stated.

I am also optimistic that Option B2 may work better for TS Conditional Types than any of the proposed alternatives, but I think that needs further exploration...

I think that, going back to #4767, B2 leaves us with a big question – do we adopt this formatting for non-nested ternaries? Doing so would certainly cause quite a lot of churn, and probably an uproar to go with it. Based on my experience, I can't imagine prettier's maintainers would be okay with that.

So I think the key question may be, would it be prohibitively surprising/confusing a jump to go from a single ternary with BOL operators to a nested ternary with EOL operators?

@DScheglov
Copy link

Can we see how Option A will work when nested in truthy branch? All I see is perfect switch-case like scenarios for that option, which doesn't really help.

See the Option description:

const complexNested =
  children && !isEmptyChildren(children) ? children : 
  props.match ? (
    component ? React.createElement(component, props) : 
    render ? render(props) : 
    null
  ) : 
  null;

@rattrayalex
Copy link
Collaborator Author

rattrayalex commented Nov 2, 2020

Exploring TS Conditional Types specifically with this format, I wonder if we could special-case things to get a nice blend of Option A and Option B2, in the spirit of @thorn0 's comment here. Specifically, B2 could turn into A when the consequent is an Identifier or scalar Literal (ie; Foo or "foo" or null or never, but not an object, array, parameterized type, conditional, etc. Or avoid special-casing and just have a group around each "line" of Option A-style TS conditionals.

For example:

type Unpacked<T> =
  T extends (infer U)[] ? U :
  T extends (...args: any[]) => infer U ?
    SomeWrapper<U> :
  T extends Promise<infer U> ? U :
    T;

What do you think @thorn0 @mmiszy?

(This approach could perhaps also apply to JS ternaries, but I'm nervous that it would be fickle – might be better to start with just TS conditionals).

@rattrayalex
Copy link
Collaborator Author

Whether or not Option B2 is adapted to turn into Option A for some TS Conditional Types, I basically agree that B2 seems very likely to be strictly superior to either fully-flat (current production behavior) or fully-indented (#9559) for TS. It scales better than fully-indented to long conditionals (which may be necessary) and looks cleaner and less confusing than fully-flat. In fact, I think TS Conditionals were often given as an example of why fully-indented wasn't good in the first place.

@thorn0
Copy link
Member

thorn0 commented Nov 2, 2020

TS conditionals are really a separate thing, for multiple reasons. What's good for them isn't necessary good for normal conditionals. For one, don't forget that it's not always T extends <complex expression> ? ... : ..., it can also be <complex expression 1> extends <complex expression 2> ? ... : ..., so this construction isn't even ternary, it's 4-ary. Also I'm sure that people who complained in #5814 mostly don't care about TS conditionals. So probably we can really leave TS conditionals alone for now and not try solving two different problems at the same time.

@rattrayalex
Copy link
Collaborator Author

I definitely see that argument, and I agree that it's fine for them to have their own logic, and for that logic to be considered separately/later. But starting from a point of consistency between ternaries and TS conditionals would definitely be preferable if possible. If the two looked totally different, that'd be unfortunate.

Also worth keeping in mind that we're racing against the clock of merging #9559, and past experience has shown that TS users don't love that behavior for TS conditionals. So I'd really like to come up with something that's better than #9559 for TS conditionals.

All that said, I basically think Option B2 fits the bill nicely, and as noted above, probably helps make it easier to merge #7948 more than anything. And I agree that whether to merge/modify/replace #7948 can be a separate question, outside of the scope of this issue.

Sound right to you?

@thorn0
Copy link
Member

thorn0 commented Nov 2, 2020

At the moment, #9559, on condition that it includes #9559 (comment), seems like the least evil to me, but I probably need to have another look at all the proposals. Also it wouldn't solve the problem with simple TS conditionals (#7940). That would still need a solution.

BTW, I've just had another thought about TS conditionals. Because of the prominent extends keyword, it's more difficult to confuse conditions and results in them, which makes the current formatting supposedly less bad for them than for normal conditionals.

@thorn0 thorn0 added lang:javascript Issues affecting JS status:needs discussion Issues needing discussion and a decision to be made before action can be taken labels Nov 2, 2020
@rattrayalex
Copy link
Collaborator Author

Hmm, I'm not sure I understand you... fully-flat is considered ugly in TS (that's what #7940 is all about), and so is fully-nested (that's what #737 was all about). Surely Option B2 is less evil, especially if it can smoothly give way to #7948 ?

@thorn0
Copy link
Member

thorn0 commented Nov 2, 2020

Fully-flat looks strange in TS only for simple types, like the example in #7940. For more complex types, as I wrote in #7948 (comment), it's pretty good.

@mike-marcacci
Copy link

mike-marcacci commented Nov 2, 2020

@rattrayalex thanks for testing out using my example. I have a couple follow-up comments based on the results and a bit more thought here. Instead of just announcing my preferences here, I think it might be more useful to explain my priorities and criteria for whatever a "good" solution here would exhibit. Maybe other folks agree or disagree:

  1. HIGH PRIORITY: All paths within the same ternary MUST be formatted under the same rules; otherwise the continuous context switching makes the logic exceedingly difficult to follow.
  2. HIGH PRIORITY: Perhaps with the exception of inlining a short, single-branch expression, ternary formatting should not change dramatically with the addition, removal, or reordering of paths; otherwise the large changes make it difficult to write (when auto-formatting is used) and difficult to review (as file diffs hide the logical change).
  3. MEDIUM PRIORITY: The total number of different formats should be small; otherwise, a developer or reviewer will have to context switch repeatedly even within the same file.

Adding a new configuration option is obviously out. However, there is precedent in prettier for applying different rules based on the manually-formatted context. For example, prettier is perfectly happy to leave both of these alone, assuming they don't break the max column width:

const a = { a: "a", b: "b" };

const b = {
  a: "a",
  b: "b"
};

Said a different way, prettier uses the existing formatting to decide whether the object's field definitions will be inline or on distinct lines.

Could we do the same for ternaries? If we use the ternary root as a cue, we could apply the "chosen" formatting to all child paths. This would allow the best format to be "chosen" by the developer and enforced by prettier without the need for arbitrary heuristics. This would satisfy my 2 biggest criteria, and leave the third in the hands of me and my team.

@rooby
Copy link

rooby commented Jan 6, 2022

A configurable option is the most sensible way forward.

As can be clearly seen in this collection of issues, there will never be agreement on what is the preferred formatting, and different developers use ternaries in different ways.
For people who use nested ternaries the old way is the obvious choice and for people who use chained ternaries and the current formatting is going to be preferable for them.
People who regularly use both would likely either prefer some inconsistent hybrid approach or else have to concede readability of one type for the other.

I know the whole “no options!” mantra will continue to be chanted but at some point you have to realise that there are multiple use cases for ternaries and forcing it into a single format for all users is counter productive (the whole purpose of prettier is productivity is it not?).
This is a legitimate use case of a configurable option.

To blindly reject a logical way forward, not because it’s a bad solution, but because it goes against your motto, is foolish.
if it is a technically bad solution that’s another story

The main prettier page says “Has few options”. An option for this could be added and that statement would still be true.
Adding an option for this doesn’t mean that it opens the flood gates and all subsequent requests for options have to be granted.

I would argue that adding an option is less bad than the alternative, which is suddenly changing the formatting of all your users’ code… again.
If something is committed that changes this to a new formatting, or even reverts back to the old one, there will be a similar outcry from people to go back to the current formatting.

@bholben
Copy link

bholben commented Jan 28, 2022

I'm pretty big on immutable coding practices and using const. I cringe when I see a let variable just so that an if/else block can reassign the value. I prefer a ternary. Unfortunately, other team members do not like using ternaries because prettier currently formats them in a way that is harder to read than the equivalent if/then. I think Option B2 does a nice job of removing this barrier.

@JonSilver
Copy link

I'm pretty big on immutable coding practices and using const. I cringe when I see a let variable just so that an if/else block can reassign the value. I prefer a ternary. Unfortunately, other team members do not like using ternaries because prettier currently formats them in a way that is harder to read than the equivalent if/then. I think Option B2 does a nice job of removing this barrier.

I couldn't have put it better myself.

@SpadeAceman
Copy link

SpadeAceman commented Feb 7, 2022

Option E: parens-style

// JavaScript

const message =
  i % 3 === 0 && i % 5 === 0 ? (
    "fizzbuzz" 
  ) : i % 3 === 0 ? (
    "fizz" 
  ) : i % 5 === 0 ? (
    "buzz" 
  ) : (
    String(i)
  );

const paymentMessage =
  state == "success" ? (
    "Payment completed successfully"
  ) : state == "processing" ? (
    "Payment processing"
  ) : state == "invalid_cvc" ? (
    "There was an issue with your CVC number"
  ) : state == "invalid_expiry" ? (
    "Expiry must be sometime in the past."
  ) : (
    "There was an issue with the payment.  Please contact support."
  );
 
const simple = 
  children && !isEmptyChildren(children) ? (
    children
  ) : (
    null
  )

const simpleChained =
  children && !isEmptyChildren(children) ? (
    children
  ) : component && props.match ? (
    React.createElement(component, props) 
  ) : render && props.match ? (
    render(props)
  ) : (
    null
  )

const complexNested =
  children && !isEmptyChildren(children) ? (
    children
  ) : props.match ? (
    component ? (
      React.createElement(component, props)
    ) : render ? (
      render(props)
    ) : (
      null
    )
  ) : (
    null
  );
// JSX

const Component = () => (
  <ul className='has-text-centered'>
    {landmarks.hits.length > 0 ? (
      landmarks.hits.map(({ name, objectID }) => (
        <li key={name}>
          <a href={`${process.env.DOMAIN}` + `/city/${objectID}`}>{name}</a>
        </li>
      ))
    ) : (
      null
    )}
  </ul>
)
// TypeScript

type TypeName<T> =
  T extends string ? (
    "string"
  ) : T extends number ? (
    "number"
  ) : T extends boolean ? (
    "boolean"
  ) : T extends undefined ? (
    "undefined"
  ) : T extends Function ? (
    "function"
  ) : (
    "object"
  );

type Unpacked<T> =
  T extends (infer U)[] ? (
    U
  ) : T extends (...args: any[]) => infer U ? (
    SomeVeryLongNameOfSomeKind<U>
  ) : T extends Promise<infer U> ? (
    U
  ) : (
    T
  );

Pros:

  1. Emulates the 1TBS curly brace formatting we're already familiar with from if-else statements, with parens filling in the role of the curly braces.
  2. As with Option B2, it's superior to Options A, C, and D in that lines with conditions are always distinct from lines with values.
  3. Improves upon Option B2 in that complex and nested ternaries don't need any special rules - the same formatting always applies. This consistency greatly aids readability and comprehension.
  4. Works with vanilla JavaScript, TypeScript, JSX, TSX...

Cons:

  1. Less concise than other proposals - it trades off terseness for improved readability and consistency.
  2. Potentially novel - I've not seen this style used or proposed anywhere else, so it's going to be unfamiliar to most people.

I've been writing my ternary expressions this way since about 2018, when I first got into functional-style JavaScript. (Ironically, it's the main reason I'm not using Prettier yet!) 😅

(Edited to add) Here's how I think my proposal meets the goals outlined by @rattrayalex :

  1. ✅ Scales well to long ternary chains.
  2. ✅ Transitions well from "single ternary" to "nested/chained ternary".
  3. ✅ Transitions well from "non-JSX" to "JSX".
  4. ✅ Works well for TypeScript Conditionals.
  5. ✅ Works well for simple nested if-else/case chains, as well as more complex situations.
  6. ❌ Causes minimal churn in existing codebases, especially those not using nested ternaries.
  7. ❌ Is concise.
  8. ✅ Clearly highlights the logical structure of the expression.

@rkrisztian
Copy link

rkrisztian commented Feb 7, 2022

@SpadeAceman , it may be more verbose, but you're touching on an excellent point that nowadays I believe ternary operators have to be more verbose anyway if we want to use them for complex expressions spanning over multiple lines. In the simplest cases, the ? and : symbols hold their fort well, but we need some more structural loudness otherwise, which is why Kotlin has hit the mark with their if-else expressions, plus also supporting blocks.

@SpadeAceman
Copy link

Oops, apparently my Option E proposal may just be how Prettier already formats JSX ternaries. 😬 After seeing this in the playground over lunch ...

      {greeting.endsWith(",") ? (
        " "
      ) : (
        <span style={{ color: "grey" }}>", "</span>
      )}

... I searched again for previous discussions on this topic, and found this comment, which seems to confirm it.

If this is indeed how things currently work, then I guess my proposal basically boils down to using Prettier's JSX-style ternaries for all multi-line ternary expressions. I would argue that formatting multi-line ternary expressions the same consistent way everywhere would be highly beneficial - just like how Prettier's consistent visual formatting provides benefits for code readability and comprehension.

@coolCucumber-cat
Copy link

Can we actually do something about it? Instead of sitting around? Let's have another vote and decide on something so I can finally remove the 10000 //prettier-ignores I need so that my code is actually readable? I even made a vscode snippet for it. Flat ternaries are so terrible.

@coolCucumber-cat
Copy link

It's been 2.5 years and there's been no update from anyone at all about this.

@sosukesuzuki
Copy link
Member

We haven't made much progress on this work because we don't have enough time. This work is progressing little by little, led by @rattrayalex. It's not dead yet. please wait.

@coolCucumber-cat
Copy link

In a week it will have been 3 years since this issue was opened. Look at this code, it's indented 3 tabs too much. Why is it just randomly indented?

const x =
	y == true
		? (() => {
				return 123
			})()
		: 456

Please just add a temporary option to add back in how it used to be, or just some temporary solution. Those that don't care about some random formatting changes in commits can use that option for now, those that do care, don't have to.

@coolCucumber-cat
Copy link

What's almost worse about this is that prettier-ignore ignores everything, so if it formats something like this badly, nothing inside it will get formatted either, so occasionally have to remove the prettier-ignore, format it, then put it back in and remove the bad formatting again.

@JonSilver
Copy link

We haven't made much progress on this work because we don't have enough time. This work is progressing little by little, led by @rattrayalex. It's not dead yet. please wait.

We're all developers here. I'm a developer too. And as developers we all know that if an outstanding improvement request or piece of technical debt hasn't been resolved after three years, it's not that "we don't have enough time", it's more a case of "this wasn't ever prioritised as something particularly important".

I'm not having a go or being stroppy, I'm merely pointing out that in a thread full of people who want a thing to be done and then waited 3 years for it, citing "not enough time" as the reason may come across as somewhat... disingenuous.

@icetbr
Copy link

icetbr commented Nov 6, 2023

I think we are conflating 2 different issues: long/complex expressions and nested ternaries. Perhaps instead of trying to find a unifying theory, we can identify usage categories, and apply one style for each. Perhaps just 2, simple vs complex.

I favor declaring long expressions in a separate variable or function:

const longValue = "There was an issue with your CVC number, and here is some extra info too.";

For me, all I want is pattern matching. So this is my favorite style, not that I'm proposing it:

const message =
  i % 3 === 0 && i % 5 === 0 ? "fizzbuzz" : 
  i % 3 === 0                ? "fizz" : 
  i % 5 === 0                ? "buzz" :
                               String(i);

const paymentMessage =
  state == "success"        ? "Payment completed successfully" :
  state == "processing"     ? "Payment processing" :
  state == "invalid_cvc"    ? "There was an issue with your CVC number, and here is some extra info too." :
  state == "invalid_expiry" ? "Expiry must be sometime in the past." :
                              "There was an issue with the payment. Please contact support.";

I'm a vertical reader, meaning I read my version as

state is success? no
state is processing ? no
state is invalid_cvs ? yes, then "There was an issue with your CVC number, and here is some extra info too.""

The other styles I read as

state is success ? if yes then then Payment completed successfully
state is processing? if yes then Payment processing
state is invalid_cc? yes, then "There was an issue with your CVC number, and here is some extra info too.""

The left part of the code is the most important part of the code. There are many names for this style, I call it column reading.

As for git concerns over white space, I think VsCode helps or eliminates the problem. Still, I commit code maybe 4-5 a week. I might read it dozens of times a day.

@Jack-Works
Copy link

I read the new post: https://prettier.io/blog/2023/11/13/curious-ternaries.html

And found the example for the new format is not much readable to me.

New format

const animalName =
  pet.canSqueak() ? 'mouse'
  : pet.canBark() ?
    pet.isScary() ?
      'wolf'
    : 'dog'
  : pet.canMeow() ? 'cat'
  : pet.canSqueak() ? 'mouse'
  : 'probably a bunny';

What I think might be better

const animalName =
  pet.canSqueak() ? 'mouse' :
  pet.canBark() ?
    pet.isScary() ? 'wolf' : 'dog' :
  pet.canMeow() ? 'cat' :
  pet.canSqueak() ? 'mouse' :
  'probably a bunny';

@bradzacher
Copy link

Something I've personally learned is that moving operators to the start of the line immensely helps with readability and navigation by to making it easier to scan code and orient oneself without reading / parsing entire lines.

My biggest issue with the new format is that it breaks that completely - it is not possible to quickly scan the left side of the new ternary to understand the composition of code.

For example taking the example from the blog post:

const animalName =
  pet.canSqueak() ? 'mouse'
  : pet.canBark() ?
    pet.isScary() ?
      'wolf'
    : 'dog'
  : pet.canMeow() ? 'cat'
  : pet.canSqueak() ? 'mouse'
  : 'probably a bunny';

Taking a subrange from that code that one might see when scanning the left-side:

  : pet.canB...
    pet.isSc...
      'wolf'

It quickly illustrates a weakness of this formatting. You can no longer understand the structure here - all you know is that the code is part of the alternate branch of a ternary. In order to glean more information (eg is there method chaining here? is there an array? is there a nested ternary?) you need to stop and read and parse more of the line to understand things and keep scanning.

In comparison the indented form of this code:

const animalName = pet.canSqueak()
  ? "mouse"
  : pet.canBark()
    ? pet.isScary()
      ? "wolf"
      : "dog"
    : pet.canMeow()
      ? "cat"
      : pet.canSqueak()
        ? "mouse"
        : "probably a bunny";

Taking the same 3 line subrange:

  : pet.canB...
    ? pet.is...
      ? "wol...

It's immediately clear what the exact structure of the code is without needing to read any further. In fact you only need to read the first character of the line to immediately understand the structure of the code.

This is the same reason that I personally prefer operators at the start of the line (#3806) over operators at the end of the line, but that's a tangent we need not get into in this discussion.

@nmn
Copy link

nmn commented Dec 11, 2023

I read the post and came here as a result. The proposed formatting is an improvement, but I dislike the mix of curious style and case style even though I find both of those styles fairly readable on their own.

My particular problem is that it makes the pattern inconsistent and harder to scan.

This would be my ideal proposal, which is essentially a small tweak of Option B2 proposed before to be consistent with what has been shipped in prettier behind the flag.

The way to read this is even simpler than the rules in the current proposal:

  • Every line that ends with a ? is an if.
  • Every : is an else
  • Everything else is a then (and is indented for readability)

This is the closest you can get to make ternaries read like a chain of if-else-if-else.

Let’s call it Option B3:

Option B2: if-then-else-style, trailing ? leading :

const message =
  i % 3 === 0 && i % 5 === 0 ?
    "fizzbuzz"
  : i % 3 === 0 ?
    "fizz"
  : i % 5 === 0 ?
    "buzz"
  : String(i);

const paymentMessage =
  state == "success" ?
    "Payment completed successfully"
  : state == "processing" ?
    "Payment processing"
  : state == "invalid_cvc" ?
    "There was an issue with your CVC number"
  : state == "invalid_expiry" ?
    "Expiry must be sometime in the past."
  : "There was an issue with the payment.  Please contact support.";

const simple =
  children && !isEmptyChildren(children) ?
    children
  : null

const simpleChained =
  children && !isEmptyChildren(children) ?
    children
  : component && props.match ?
    React.createElement(component, props)
  : render && props.match ?
    render(props)
  : null

const complexNested =
  children && !isEmptyChildren(children) ?
    children
  : props.match ?
    component ?
      React.createElement(component, props)
    : render ?
      render(props)
    : null
  : null;

type TypeName<T> =
  T extends string ?
    "string"
  : T extends number ?
    "number"
  : T extends boolean ?
    "boolean"
  : T extends undefined ?
    "undefined"
  : T extends Function ?
    "function"
  : "object";

type Unpacked<T> =
  T extends (infer U)[] ?
    U
  : T extends (...args: any[]) => infer U ?
    SomeVeryLongNameOfSomeKind<U>
  : T extends Promise<infer U> ?
    U
  : T;

// EDIT: More examples from Theo
type Something<T> =
  T extends string ?
    "string"
  : T extends number ?
    "number"
  : "other";
  
type Something<T> =
  T extends string ?
    T extends "a" ?
      "a"
    : "b"
  : T extends number ?
    "number"
  : "other";

// Very complex example.
type Schema<TParams> =
  TParams['_input_out'] extends UnsetMarker ?
    $Parser
  : inferParser<$Parser>['out'] extends Record<string, unknown> | undefined ?
    TParams['_input_out'] extends Record<string, unknown> | undefined ?
      undefined extends inferParser<$Parser>['out'] ?
        undefined extends TParams['_input_out'] ?
          $Parser
        : ErrorMessage<"Cannot chain an optional parser to a required parser">
      : $Parser
    : ErrorMessage<"A;; input parsers did not resolve to an object">
  : ErrorMessage<"A;; input parsers did not resolve to an object">;

Pros

It’s easier to tell which lines are conditions and which lines are values: It reads almost exactly like if-else. Every like that ends with ? is a condition. : can be read as else. Every indented line is a then.

For complex “then” values, parenthesis can still be used in a clean readable way.

It’s already what the proposed formatting would generate as long as the conditions for long enough to trigger wrapping.

Cons

Nested ternaries in the “then” case lead to multiple levels of nesting hurting readbility.
This is a problem with every formatting option.

Increases the vertical height of the code.

Further improvement to readability?

In my opinion, wrapping any nested ternary expression in parenthesis further helps the readability at the cost of even more vertical space. Here in an example with and without parenthesis.

// Without parenthesis for nested ternary:
const complexNested =
  children && !isEmptyChildren(children) ?
    children
  : props.match ?
    component ?
      React.createElement(component, props)
    : render ?
      render(props)
    : null
  : null;

// With parenthesis for nested ternary:
const complexNested =
  children && !isEmptyChildren(children) ?
    children
  : props.match ?
    ( component ?
      React.createElement(component, props)
    : render ?
      render(props)
    : null )
  : null;

// With parenthesis where every line that start with punctuation is a condition
const complexNested
  = children && !isEmptyChildren(children) ?
    children
  : props.match ?
    ( component ?
      React.createElement(component, props)
    : render ?
      render(props)
    : 
      null 
    )
  :
    null
  ;
// This is the most readable IMO. 

// Here's the Very complex example with parens.
type Schema<TParams>
  = TParams['_input_out'] extends UnsetMarker ?
    $Parser
  : inferParser<$Parser>['out'] extends Record<string, unknown> | undefined ?
    ( TParams['_input_out'] extends Record<string, unknown> | undefined ?
        ( undefined extends inferParser<$Parser>['out'] ?
          ( undefined extends TParams['_input_out'] ?
            $Parser
          : 
            ErrorMessage<"Cannot chain an optional parser to a required parser">
          )
        : 
          $Parser
        )
    : 
      ErrorMessage<"A;; input parsers did not resolve to an object">
    )
  : 
    ErrorMessage<"A;; input parsers did not resolve to an object">
  ;

I don't have a strong feeling about the () for the nested ternaries, just wanted to share the option.

With the last variant, I think the rules become very simple:

  1. Every condition starts with a punctuation
  2. : is else.

@trusktr
Copy link

trusktr commented Dec 15, 2023

Currently I get this:

this[prop.name] = !handler
	? newVal
	: newVal === null // attribute removed
	? 'default' in handler
		? handler.default
		: null
	: handler.from
	? handler.from(newVal)
	: newVal

but I manually change it to this which is more readable to me:

// prettier-ignore
this[prop.name] = !handler
	? newVal
	: newVal === null // attribute removed
		? 'default' in handler
			? handler.default
			: null
		: handler.from
			? handler.from(newVal)
			: newVal

(sorry GitHub renders tabs so wide)

The second is cleaner because you can clearly see each ternary. Has that format been considered?

@zeorin
Copy link

zeorin commented Dec 15, 2023

I think, 5 years later, after multiple changes to ternary formatting that always sufficiently annoy a significant portion of users, it may be time to accept that there is no one-size-fits-all ternary format.

Creating an option to allow a user to choose a style would be great.

Allowing for the community to create plugins for different styles would also be a way to allow for flexibility (or is this already possible with the current plugin API?).

@SpadeAceman
Copy link

I think, 5 years later, after multiple changes to ternary formatting that always sufficiently annoy a significant portion of users, it may be time to accept that there is no one-size-fits-all ternary format.

Agreed.

Reading the Option Philosophy page, this sentence stood out to me:

Now that Prettier is mature enough and we see it adopted by so many organizations and projects, the research phase is over.

The existence of the --experimental-ternaries option (on top of so many discussions and formatting changes over the years) tells me that, when it comes to ternary formatting, the research phase is not, in fact, over.

So, if we admit that this is still an area of active and ongoing research, then the potential need for configuration in this area can't be dismissed out of hand. Especially if this ongoing research ultimately concludes (as I believe it will) that no one-size-fits-all ternary format is possible.

@DScheglov
Copy link

Has someone consulted about formatting of nested ternarries with any AI?
Just curious...

I still vote for flatting else-branches and indeting the if branches, and consider the adding/removing one case must affect only this case.

By the way, it is also a good idea to use ternarries like tables (but in case of else-branching only and if the max-len is respected), where the conditions are placed in the first colum, then-branches are placed in the second one, the ? splits the first and second columns and the : is a right border of the table, like it is pointed here: #9561 (comment)

const message =
  i % 3 === 0 && i % 5 === 0 ? "fizzbuzz" : 
  i % 3 === 0                ? "fizz" : 
  i % 5 === 0                ? "buzz" :
                               String(i);

const paymentMessage =
  state == "success"        ? "Payment completed successfully" :
  state == "processing"     ? "Payment processing" :
  state == "invalid_cvc"    ? "There was an issue with your CVC number, and here is some extra info too." :
  state == "invalid_expiry" ? "Expiry must be sometime in the past." :
                              "There was an issue with the payment. Please contact support.";

@olosegres
Copy link

olosegres commented Jan 1, 2024

Why don't we make them even more like an if-else block? (Like prettier do it for jsx)

Look how easy it is to read:

const animalName =
  pet.canSqueak() ? (
    'mouse'
  ) : (
    pet.canBark() ? (
      pet.isScary() ? (
        'wolf'
      ) : (
        'dog'
      )
    ) : (
      pet.canMeow() ? (
        'cat'
      ) : (
        pet.canSqueak() ? (
          'mouse'
        ) : (
          'probably a bunny'
        )
      )
    )
  );

UPD, shortened form (adding braces only when there is not a primitive value)

const animalName =
  pet.canSqueak() ? 'mouse' : (
    pet.canBark() ? (
      pet.isScary() ? 'wolf' : 'dog'
    ) : (
      pet.canMeow() ? 'cat' : (
        pet.canSqueak() ? 'mouse' : 'probably a bunny'
      )
    )
  );

@DScheglov
Copy link

@olosegres
I voted against. My personal opinion regarding your personal is the following:
-- too many unnecessary braces: for jsx it makes sense, but for primitive values doesn't
-- the indents cause unwanted git-diffs on adding/removing a single case/condition/branch

@olosegres
Copy link

@DScheglov I've added example with shortened form, what do you think about it?

p.s. we spend a lot of time understanding already written logic, which means by improving this we win, even if there is something wrong with git-diffs

@nmn
Copy link

nmn commented Jan 2, 2024

@olosegres See my proposal above which is already modelling if-else but using only indentation.

In your updated case, I am against the "then" case ever being on the same line as that hurts scannability. The other nit-pick with your proposal is that it doesn't model "else if". Instead it's an "if" nested within an else.

Here's your example updated to my proposal which reads like if, else-if, else to me. The big con is that it takes a bunch of vertical space. I think this OK for any non simple ternary.

const animalName 
  = pet.canSqueak() ? 
    'mouse' 
  : pet.canBark() ?
    ( pet.isScary() ? 'wolf' : 'dog' ) // Simple cases on 1-line.
  : pet.canMeow() ? 
    'cat' 
  : pet.canSqueak() ? 
    'mouse' 
  :                                    // empty line for clear "or else" case.
    'probably a bunny' 
  ;                                    // semi-colon for a clear "end"

Every line that starts with a punctuation is an "if" every ":" is an "else".

@bouzidanas
Copy link

bouzidanas commented Jan 26, 2024

I am in agreement with @bradzacher #9561 (comment)

But I would also consider a few additional adjustments I ended up employing to help with readability and navigation.

Example code

info.type === "playlist"
?    info.creationDate
     ?    <div className="lt-info-stats">
               <span className="text pure">Created on {info.creationDate}</span>
           </div>
     :   null
:    info.type === "artist"
     ?    <div className="lt-info-stats">
               <span className="text pure">{info.genre}</span>
           </div>
     :    <div className="lt-info-stats">
              <span className="text pure">{info.releaseDate}</span>
              <span className="cdot" style={{ fontWeight: "bold", margin: "1px" }}>·</span>
              <span className="text pure">{info.genre}</span>
          </div>

There are two adjustments made in the above example:

  1. The question mark is made to line up with the beginning character of every conditional. This adds consistency in visual alignment (of what is visible and invisible like spaces and tabs)
  2. A tab (4 spaces) is added after each? or :

These adjustments help be visually separate the elements into blocks much like the brackets in an if/else:
ternary-operator-visual-blocks

Edit

Over some time, I have switched between the adjustments I added and the formatting @bradzacher shows in his last examples and I have changed my opinion. While my minor adjustments are more easily digestible visually to me, I realized that

  1. The difference is very minor and not hard to get used to and
  2. My adjustments clash with prettier indentation of other things.

So, I am currently inclined to retracted my suggestions...

Fyi, here is a little gif showing the visual difference:

ternary-formatting-vs-prettier

@tsnelson1
Copy link

Here is my approach I like to use to format ternary operators. I find this works well because of the way the layout follows these principals:

  1. Each line immediately identifies itself as a ternary operator
  2. It restricts each line to 1 condition and/or one value making it easy to follow the logic
  3. It is still easy to format even nested complex if/else conditions
  4. It is more concise and if you choose, you could format each line where the value starts at the same position on each line

The cons as I see it are:

  1. If the condition clause is long, it would be hard to see the value
  2. If the value is long, it may overrun a formatted line restriction
  3. If the condition or value is multiple lines, my preference would then be to fallback to if/else
const message =  i % 3 === 0 && i % 5 === 0 ? "fizzbuzz" 
  : i % 3 === 0 ? "fizz"
  : i % 5 === 0 ? "buzz"
  : String(i);

const paymentMessage = state == "success" ? "Payment completed successfully"
  : state == "processing" ? "Payment processing"
  : state == "invalid_cvc" ? "There was an issue with your CVC number"
  : state == "invalid_expiry" ? "Expiry must be sometime in the past."
  : "There was an issue with the payment.  Please contact support.";

const simple = children && !isEmptyChildren(children) ? children 
  : null

const simpleChained = children && !isEmptyChildren(children) ? children 
  : component && props.match ? React.createElement(component, props)
  : render && props.match ? render(props) 
  : null

const complexNested = children && !isEmptyChildren(children) ? children 
  : props.match ? component
    ? React.createElement(component, props)
    : render ? render(props)
    : null 
  : null;

type TypeName<T> = T extends string ? "string"
  : T extends number ? "number"
  : T extends boolean ? "boolean"
  : T extends undefined ? "undefined"
  : T extends Function ? "function"
  : "object";

type Unpacked<T> = T extends (infer U)[] ? U
  : T extends (...args: any[]) => infer U ? SomeVeryLongNameOfSomeKind<U>
  : T extends Promise<infer U> ? U
  : T;

@DScheglov
Copy link

@tsnelson1 hi,

I'm not sure that it works as you described:

const message = i % 3 === 0 && i % 5 === 0 ? "fizzbuzz" 
  : i % 3 === 0 ? "fizz"
  : i % 5 === 0 ? "buzz"
  : String(i);

The rule "Each line immediately identifies itself as a ternary operator" doesn't work for the first line.

To match the rule your example must look like this:

const message = false ? assertUnreachable()
  : i % 3 === 0 && i % 5 === 0 ? "fizzbuzz" 
  : i % 3 === 0 ? "fizz"
  : i % 5 === 0 ? "buzz"
  : String(i);

Why do I think so. Becuse when we are talking about identifies we don't consider the identification requires to read the entire line, what is required in your example for the first line of the ternary expression.

Additionally it is not a good practice to have two ways to do the same.
We need to find the ? for the first line, but for the rest lines we need to meet : as the first non-space character.

We already discussed in this or similar threads more consistent approaches, as instance the following;

const message =  
  i % 3 === 0 && i % 5 === 0 ? "fizzbuzz" :
  i % 3 === 0 ? "fizz" :
  i % 5 === 0 ? "buzz" :
  String(i);

or like this

const message =  
   i % 3 === 0 && i % 5 === 0 ? "fizzbuzz"  :
   i % 3 === 0                ? "fizz"      :
   i % 5 === 0                ? "buzz"      :
                                String(i);

Sometimes I use the following formating:

const paymentMessage =
  // prettier-ignore
  state === "success"
    ? "Payment completed successfully" :
  state === "processing"
    ? "Payment processing" :
  state === "invalid_cvc"
    ? "There was an issue with your CVC number" :
  state === "invalid_expiry"
    ? "Expiry must be sometime in the past." :
  // otherwise
      "There was an issue with the payment.  Please contact support."
;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
lang:javascript Issues affecting JS status:needs discussion Issues needing discussion and a decision to be made before action can be taken
Projects
None yet
Development

No branches or pull requests