dprint-plugin-typescript icon indicating copy to clipboard operation
dprint-plugin-typescript copied to clipboard

Make JSX formatting inside ternary expressions consistent

Open ThiefMaster opened this issue 4 months ago • 4 comments

I have this snippet (formatted by prettier):

const foo = cond ? (
  <Foo>
    <X />
  </Foo>
) : (
  <Bar />
);

which gets formatted like this:

const foo = cond ?
  (
    <Foo>
      <X />
    </Foo>
  ) :
  <Bar />;

Or another example:

<Message>
  {type === 'error' ? (
    <Icon name="alert" />
  ) : type === 'warning' ? (
    <Icon name="exclamation" />
  ) : (
    <Icon name="info" />
  )}
</Message>

gets formatted as:

<Message>
  {type === 'error' ?
    <Icon name="alert" /> :
    type === 'warning' ?
    <Icon name="exclamation" /> :
    <Icon name="info" />}
</Message>;

I know formatting is subjective, but this looks quite bad. Previously both branches were consistently on the same indentation level. Now it adds another level of indentation just for the parentheses, instead of keeping it after the ?.

Would it be possible to make this behavior configurable? Subjective preferences aside, it would also help a lot for all those who want to get rid of prettier - nobody likes huge amounts of whitespace/indentation changes, they completely ruin git blame and give you very nasty merge/rebase conflicts in case you have lots of open PRs (and which large project doesn't...)


My config (mostly default, just a few changes):

{
  "lineWidth": 100,
  "indentWidth": 2,
  "useTabs": false,
  "semiColons": "always",
  "quoteStyle": "preferSingle",
  "jsx.quoteStyle": "preferDouble",
  "quoteProps": "consistent",
  "newLineKind": "lf",
  "useBraces": "always",
  "bracePosition": "sameLineUnlessHanging",
  "singleBodyPosition": "nextLine",
  "nextControlFlowPosition": "sameLine",
  "trailingCommas": "onlyMultiLine",
  "operatorPosition": "sameLine",
  "preferHanging": false,
  "preferSingleLine": false, // check behavior
  "arrowFunction.useParentheses": "preferNone",
  "binaryExpression.linePerExpression": true, // check behavior
  "jsx.bracketPosition": "nextLine",
  "jsx.forceNewLinesSurroundingContent": false,
  "jsx.multiLineParens": "prefer",
  "memberExpression.linePerExpression": true, // check behavior
  "typeLiteral.separatorKind": "semiColon", // check with team
  "enumDeclaration.memberSpacing": "newLine",
  "spaceAround": false,
  "spaceSurroundingProperties": false,
  "binaryExpression.spaceSurroundingBitwiseAndArithmeticOperator": true,
  "commentLine.forceSpaceAfterSlashes": true,
  "constructor.spaceBeforeParentheses": false,
  "constructorType.spaceAfterNewKeyword": false,
  "constructSignature.spaceAfterNewKeyword": false,
  "doWhileStatement.spaceAfterWhileKeyword": true,
  "exportDeclaration.spaceSurroundingNamedExports": false,
  "forInStatement.spaceAfterForKeyword": true,
  "forOfStatement.spaceAfterForKeyword": true,
  "forStatement.spaceAfterForKeyword": true,
  "forStatement.spaceAfterSemiColons": true,
  "functionDeclaration.spaceBeforeParentheses": false,
  "functionExpression.spaceBeforeParentheses": false,
  "functionExpression.spaceAfterFunctionKeyword": false,
  "getAccessor.spaceBeforeParentheses": false,
  "ifStatement.spaceAfterIfKeyword": true,
  "importDeclaration.spaceSurroundingNamedImports": false,
  "jsxSelfClosingElement.spaceBeforeSlash": true,
  "jsxExpressionContainer.spaceSurroundingExpression": false,
  "method.spaceBeforeParentheses": false,
  "setAccessor.spaceBeforeParentheses": false,
  "taggedTemplate.spaceBeforeLiteral": false,
  "typeAnnotation.spaceBeforeColon": false,
  "typeAssertion.spaceBeforeExpression": true,
  "whileStatement.spaceAfterWhileKeyword": true,
  "module.sortImportDeclarations": "maintain", // check codebase for impact of `caseInsensitive`
  "module.sortExportDeclarations": "maintain", // check codebase for impact of `caseInsensitive`
  "exportDeclaration.sortNamedExports": "maintain", // check codebase for impact of `caseInsensitive`
  "exportDeclaration.sortTypeOnlyExports": "none",
  "importDeclaration.sortNamedImports": "maintain", // check codebase for impact of `caseInsensitive`
  "importDeclaration.sortTypeOnlyImports": "none",
  "ignoreNodeCommentText": "dprint-ignore",
  "ignoreFileCommentText": "dprint-ignore-file",
  "exportDeclaration.forceSingleLine": false,
  "importDeclaration.forceSingleLine": false,
  "exportDeclaration.forceMultiLine": "never",
  "importDeclaration.forceMultiLine": "never",
  // compat with our existing prettier style
  "arguments.trailingCommas": "never",
  "conditionalExpression.operatorPosition": "maintain",
  "conditionalExpression.preferSingleLine": true,
}

Playground link

ThiefMaster avatar Aug 14 '25 17:08 ThiefMaster

OK, so the first issue is partially avoided by setting conditionalExpression.linePerExpression to false. This is not documented BTW, I just found it while looking into the code.

With this change I get this:

const foo = cond ? (
  <Foo>
    <X />
  </Foo>
) : <Bar />;

ThiefMaster avatar Aug 18 '25 11:08 ThiefMaster

Ah, but this affects many more places. So not ideal. IMHO for the - pretty common - case of a conditional expression w/ JSX it would make more sense to not consider the parentheses part of the expression, but only their content...

ThiefMaster avatar Aug 18 '25 11:08 ThiefMaster

Another case that looks quite weird (and IMHO deserves configurability):

<Foo
  fixed="test"
  short={test ? a : b}
  long={test ? 'this is something longer' : 'foo'}
/>

is currently formatted like this (lineWidth=30):

<Foo
  fixed="test"
  short={test ? a : b}
  long={test
    ? 'this is something longer'
    : 'foo'}
/>;

however, not having anything in the line where the attribute name starts would be much better:

<Foo
  fixed="test"
  short={test ? a : b}
  long={
    test
      ? "this is something longer"
      : "foo"
  }
/>

ThiefMaster avatar Aug 19 '25 09:08 ThiefMaster

FYI my current code that kind of works for me (it also breaks up stuff that would fit nicely in a single line, I could not figure out how to avoid this so far) is here: https://github.com/dprint/dprint-plugin-typescript/compare/main...ThiefMaster:dprint-plugin-typescript:jsx-ternary-improvements

ThiefMaster avatar Aug 20 '25 11:08 ThiefMaster