Skip to content

Conversation

rattrayalex
Copy link
Collaborator

@rattrayalex rattrayalex commented Oct 31, 2020

Fixes #5814

This is an alternative to #9559; please upvote this PR or #9559, or a proposal in #9561

I think this PR has better behavior since it scales better to long ternary formats, has an extremely clear visual difference between conditionals and consequents, and is easy to refactor. I've also had good experiences and heard good feedback on the JSX style when it is used in JSX.

Headline examples from the tests:

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 StorybookLoader = ({ match }) =>
  match.params.storyId === "button" ? (
    <ButtonStorybook />
  ) : match.params.storyId === "color" ? (
    <ColorBook />
  ) : match.params.storyId === "typography" ? (
    <TypographyBook />
  ) : match.params.storyId === "loading" ? (
    <LoaderStorybook />
  ) : match.params.storyId === "deal-list" ? (
    <DealListStory />
  ) : (
    <Message>
      <Title>{"Missing story book"}</Title>
      <Content>
        <BackButton />
      </Content>
    </Message>
  );

and a case I hope to add to the tests:

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

Note that this PR uses indents for TS Conditional Types, eg:

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

but we could easily back that change out.

I may seek to change TS Conditional Type formatting in a follow-on PR, and am considering this case-style format:

type Animal<T> =
  T extends Quacker ? Duck :
  T extends SuperTall ? Giraffe :
  T extends SingleCelled ? (
    T extends Eukaryotic ? Amoeba :
    Bacteria
  ) :
  T extends Barker ? Dog :
  GenericAnimal;
  • I’ve added tests to confirm my change works.
  • (If changing the API or CLI) I’ve documented the changes I’ve made (in the docs/ directory)
  • (If the change is user-facing) I’ve added my changes to changelog_unreleased/*/pr-XXXX.md file following changelog_unreleased/TEMPLATE.md.
  • I’ve read the contributing guidelines.

Try the playground for this PR

@thorn0
Copy link
Member

thorn0 commented Oct 31, 2020

Another reason to prefer this style is tabs. "Use tabs for indentation, spaces for alignment" sounds good in theory but is confusing in practice. I avoid using tabs now because Prettier works best with spaces, specifically because of how it formats ternaries and union types (see #4198).

Also I totally agree that if the main problem with the current formatting is not the indentation per se but the lack of a clear separation between conditions and results, then this style is a tidier way to achieve this separation than aggressive indentation.

@DScheglov
Copy link

@rattrayalex , hey

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

it is much more noisy then ts conditional type style:

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

Could "noise" be considered too?

@askuzminov
Copy link

askuzminov commented Nov 1, 2020

Original

const pickBgColor = (colorName: string) =>
  colorName === 'active'
    ? colorName === 'active'
      ? 'var(--blue2)'
      : colorName === 'inactive'
    : colorName === 'inactive'
    ? 'var(--blue1)'
    : colorName === 'success' || colorName === 'abc'
    ? colorName === 'abc'
      ? 'var(--blue2)'
      : 'var(--blue23)'
    : 'var(--red4)';

const message = (i: number) =>
  i % 3 === 0 && i % 5 === 0
    ? 'fizzbuzzfizzbuzzfizzbuzzfizzbuzz'
    : i % 3 === 0
    ? i % 3 === 0
      ? 'ffizzbuzzfizzbuzzfizzbuzzfizzbuzzizz'
      : i % 5 === 0
    : i % 5 === 0
    ? 'fizzbuzzfizzbuzzfizzbuzzfizzbuzz'
    : String(i);

current PR

const pickBgColor = (colorName: string) =>
  colorName === "active" ? (
    colorName === "active" ? (
      "var(--blue2)"
    ) : (
      colorName === "inactive"
    )
  ) : colorName === "inactive" ? (
    "var(--blue1)"
  ) : colorName === "success" || colorName === "abc" ? (
    colorName === "abc" ? (
      "var(--blue2)"
    ) : (
      "var(--blue23)"
    )
  ) : (
    "var(--red4)"
  );

const message = (i: number) =>
  i % 3 === 0 && i % 5 === 0 ? (
    "fizzbuzzfizzbuzzfizzbuzzfizzbuzz"
  ) : i % 3 === 0 ? (
    i % 3 === 0 ? (
      "ffizzbuzzfizzbuzzfizzbuzzfizzbuzzizz"
    ) : (
      i % 5 === 0
    )
  ) : i % 5 === 0 ? (
    "fizzbuzzfizzbuzzfizzbuzzfizzbuzz"
  ) : (
    String(i)
  );

The original is much better. The current PR is terrible. There is a lot of noise here, like in JSX.
These brackets only get in the way.

@rattrayalex
Copy link
Collaborator Author

Having examples is helpful, thank you – especially when they are more real-world / have sensible code.

I am now convinced that the "JSX style approach" is too simplistic to work well everywhere, so I will close this PR.

I am however still optimistic (possibilistic?) that a solution may exist which truly improves the readability of nested/chained ternaries everywhere.

@DScheglov , I am also starting to think that case-style might be a good starting point in general, eg:

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

I'm also interested in the if-else-style approach outlined here by @JAForbes, eg:

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

I've opened a separate issue to muse about the possibilities at greater length: #9561

@rattrayalex rattrayalex closed this Nov 1, 2020
@lydell
Copy link
Member

lydell commented Nov 1, 2020

@rattrayalex I draw the opposite conclusion from the provided examples (only one of which seems to be real-world code). In my opinion, the JSX style was easier to read in all cases. Longer, yes. More parens, yes. But not noisier than good ol’ if-else.

I think we should reopen and at least give it a chance.

@rattrayalex rattrayalex reopened this Nov 1, 2020
@rattrayalex
Copy link
Collaborator Author

rattrayalex commented Nov 1, 2020

@lydell fair, done!

Though I gotta say, I really like the case-style when things are simple, and it naturally turns right into ~JSX-style when they're not:

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

const message =
  i % 3 === 0 && i % 5 === 0 ? (
    // pretend this is big and complex
    "fizz" + "buzz" 
  ) :
  i % 3 === 0 ? (
    // this too
    "fizz" 
  ) :
  i % 5 === 0 ? (
    // such a long line wow
    "buzz" 
  ) : (
    // and this
    String(i)
  );

The only difference being that ) : is on its own line in this sketch, but we could change that and have that be on the same line as the next thing, eg:

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

@rattrayalex
Copy link
Collaborator Author

@lydell btw, a big part of my motivation in opening #9561 is handling TS Conditional types nicely, which IMO look really bad with JSX-style:

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
  );

if members of the TS community disagree and find this the most readable option, I'd be quite happy to put this back on the table!

@DScheglov
Copy link

@rattrayalex , I've posted real-world example in the Issue, see the comment:
#5814 (comment)

It is absolutely real.

@thorn0
Copy link
Member

thorn0 commented Nov 1, 2020

@rattrayalex WRT conditional types, see #7948 for some good code examples.

@rattrayalex
Copy link
Collaborator Author

Just musing aloud, I wonder about an "all-or-nothing" approach to chained ternaries, where if none of the ternaries break, you get this:

const pickBgColor = (colorName) =>
  colorName === 'active' ? 'var(--blue2)' :
  colorName === 'inactive' ? 'var(--blue1)' :
  colorName === 'success' ? 'var(--green4)' :
  'var(--red4)';

but if any of them are too long for one line, they all become JSX-style...

@thorn0
Copy link
Member

thorn0 commented Nov 2, 2020

@rattrayalex I proposed a similar thing in #7948 for conditional types, but for normal (non-type) conditionals, it might be too diff-unfriendly to reformat an entire multiline expression upon changing only a small part of it.

@rattrayalex
Copy link
Collaborator Author

Ah, yes, that's a very good point. Really changes how I think about this to be honest.

@DScheglov
Copy link

Ah, yes, that's a very good point. Really changes how I think about this to be honest.

Hey, @rattrayalex , any option we choose (except keeping as is) will be diff-unfriendly, ...
It seems to be needed an setting.

And one more option -- it is difficult, but possible. Prettier analyzes the source code and defines "current" style and after that checks style-consistency.

@rattrayalex
Copy link
Collaborator Author

Sorry, to be clear, the diff changes we are talking about are while editing code once this is released, not "at release of this change".

Constant large diffs while editing code, in places unrelated to the line you're editing, happen sometimes with prettier but are great to avoid whenever possible.

I appreciate the desire to be helpful, but adding new settings or having prettier "keep existing style" are antithetical to prettier's philosophy. As you can see from countless threads, prettier would rather make some (even many) people unhappy with how their code looks than adopt either approach. I am not a core maintainer; I don't make these rules, I just have to play by them.

@thorn0
Copy link
Member

thorn0 commented Nov 2, 2020

Adding a setting also means admitting that all the proposals are equally good/bad, so the whole problem is nothing more but bike shedding, in which case we can as well simply do nothing. I hope it's not the case though.

Base automatically changed from master to main January 23, 2021 17:13
@rattrayalex
Copy link
Collaborator Author

I no longer think this is a good idea, should have closed this a while ago.

I now believe that postfixed ternaries are the only scalable, readable, consistent alternative, proposed here as "Option B2": #9561 (comment)

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Apr 19, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Nested ternary formatting - add indents back
6 participants