prettier: Typescript/Flow: Inconsistent union and intersection type multiline formatting

Prettier 1.10.2 Playground link

--parser typescript

Input:

type IntesectionType = VeryLongTypeA & VeryLongTypeB & VeryLongTypeC & VeryLongTypeD & VeryLongTypeE

type UnionType = VeryLongTypeA | VeryLongTypeB | VeryLongTypeC | VeryLongTypeD | VeryLongTypeE

Output:

type IntesectionType = VeryLongTypeA &
  VeryLongTypeB &
  VeryLongTypeC &
  VeryLongTypeD &
  VeryLongTypeE;

type UnionType =
  | VeryLongTypeA
  | VeryLongTypeB
  | VeryLongTypeC
  | VeryLongTypeD
  | VeryLongTypeE;

Expected behavior:

type IntesectionType = 
  & VeryLongTypeA
  & VeryLongTypeB
  & VeryLongTypeC
  & VeryLongTypeD
  & VeryLongTypeE;

type UnionType =
  | VeryLongTypeA
  | VeryLongTypeB
  | VeryLongTypeC
  | VeryLongTypeD
  | VeryLongTypeE;

I think the Interections type should use the same format as the one used for the Union type.

About this issue

  • Original URL
  • State: open
  • Created 6 years ago
  • Reactions: 38
  • Comments: 17 (6 by maintainers)

Most upvoted comments

The proposed behavior is growing on me. I see the argument about

type bar = foo & {
  baz: number
}

but that could also be special cased.

What I wrote:

type IO =
  & (
    | { type: "in"; opts: InputOpts }
    | { type: "out"; opts: OutputOpts }
  )
  & (
    | { mode: "file"; file: string }
    | { mode: "buffer" | "stdio"; stream: PassThrough }
  )

What came out:

type IO = (
  | { type: "in"; opts: InputOpts }
  | { type: "out"; opts: OutputOpts }) &
  (
    | { mode: "file"; file: string }
    | { mode: "buffer" | "stdio"; stream: PassThrough })

😕

Also I just noticed that with --parser flow the output is much better

This is still a problem, IMO. E.g. the following code constructs a type, which includes properties from type A except those present in Bar or given object type (effectively { ...A, ...Bar, ...{ baz: string } }):

type Foo<A> = Pick<A, Exclude<keyof A, 'baz' | keyof Bar>> & Bar & { baz: string };

The intersection expression has three members: Pick<...>, { ... } and Bar, and I expect to see each one on a separate line (if the whole expression doesn’t fit print width):

// Print width: 60.
type Foo<A> =
  Pick<A, Exclude<keyof A, 'baz' | keyof Bar>> &
  { baz: string } &
  Bar;

But Prettier wraps it in pretty unreadable and non-sematic manner:

// Print width: 60.
type Foo<A> = Pick<
    A,
    Exclude<keyof A, 'baz' | keyof Bar>
> & { baz: string } & Bar;
// ↑ What the hell is this? How can I be able to read it?

FYI: Prettier wraps non-type expressions in desired manner:

// RHS expression is almost identical to the RHS expression from above:
const Foo = Pick(A, Exclude(keyof(A), 'baz' | keyof(Bar))) & { baz: string } & Bar;

becomes

// Print width: 60.
const Foo =
    Pick(A, Exclude(keyof(A), 'baz' | keyof(Bar))) &
    { baz: string } &
    Bar;

Also, I agree with @albertorestifo that formatting with leading & is preferable, but it isn’t critical.

It would be nice to special-case it for |, too.

Just set up prettier in a large-scale project, and this was definitely a bizarre issue to run into. I would love to see this revisited. The inconsistency between intersection and union has definitely been a hit in readability for our team. Also, my biggest frustration with it is we have a number of intersections that we regularly add to, and not being able to just copy the last item down and edit it is pretty annoying

Also, as mentioned above, the primary case that seems to be used to justify the inconsistency (shown below) seems easy enough to special case, and probably should be special-cased for both intersections and unions. It certainly doesn’t feel like there is enough benefit to warrant the inconsistency, and it seems to be less readable in almost anything besides the below use case

https://github.com/prettier/prettier/issues/3986#issuecomment-369059369

type bar = foo & {
  baz: number
}

I can’t help but think that it would be a lot simpler maintenance-wise to just treat intersections the same as unions.

Currently intersections have their own unique logic so can’t easily be standardised to format the same as binary expressions - they’re going to need their own, custom logic in order to look good - which will increase the maintenance burden over time as more edge cases are found (and introduced by new syntax). Example of bad formatting just for intersections: https://github.com/prettier/prettier/issues/14726, #14773.

If we instead formatted multiline intersections the same as unions - with a leading & and each member on its own line - then there would no longer be any additional branches to maintain.

I wonder if it’s time to revisit this change with the looming v3 major release? A major release would be the perfect time to make this sort of change.


As an aside - it’s worth noting that this sort of thing doesn’t impact flow that much now-a-days because the vast, vast majority of flow object types are merged via spreads {...T, ...U} and intersecting them often leads to weird behaviour in the type system. This is in contrast to TS where there is no spread syntax so the only options for merging object types is by intersecting them or by using interface extends.

Hi everyone,

I don’t really understand why https://github.com/prettier/prettier/pull/3988 has not been accepted, and what is intentional by having the current formatting behavior. When you start having complexe type (with options, inferences, etc) the current formatting prevent the developer to quickly understand your code and eventually end by being unmaintainable.

The arguments put forwards are:

  1. not seen anyone using leading &
  2. looks like a binary operator

For the 1st one, lot of people would like but can’t as we mostly use prettier for our syntax formatting, and who’s doesn’t use tools for formatting are probably not a good example to follow 😅

For the 2nd one, it is the same issue with the pipe | operator. BTW we use this operators for intersection and union because they behave as it would on binary numbers. If there was a XOR (^) operator for typescript types it would work (for object/record it would means “union + removing same keys”).

So, can we debates on this and close this issue ?

Thanks

@tommarien we can fix it without milestone 3

i didn’t realize the original issue was for typescript. but i guess it’s still relevant for flow as well 😃

Seems pretty straight forward to fix. I’ll try to make a PR during the weekend