Repository: styleguidist/react-docgen-typescript
Pull request #503 (2024-05-20 17:47:16)

Add option for sorting union members
## Background

I have a problem in Storybook docs where the order of union members is unpredictable. As far as I can tell, it's deterministic, but I just can't determine what it will be from build to build. A single commit of our code is consistent, but unrelated changes to our codebase can end up changing the order in which strings in a union are printed out in our prop tables. This is more than an annoyance because I have regression tests around the content of the documentation, and I can't lock onto it with a snapshot if the order keeps changing; it gives me false negatives all the time.

## Summary of changes

I've added a `shouldSortUnions` configuration option that, when used in combination with `shouldExtractValuesFromUnion` will make sure the members are sorted in a consistent way (string sort order).

Fixes https://github.com/styleguidist/react-docgen-typescript/issues/302

## Verification

I wrote unit tests, but I was unable to get the examples running due to some outdated dependencies. I did verify my changes in Storybook, however. See the screenshots below. In order to set the correct options for this library from the Storybook config, I had to extend the defaults like this:

```ts
import { typescript as typescriptPresets } from '@storybook/core-server/dist/presets/common-preset';

const config: StorybookConfig = {
  typescript: {
    reactDocgen: 'react-docgen-typescript',
    reactDocgenTypescriptOptions: {
      ...typescriptPresets().reactDocgenTypescriptOptions,
      shouldSortUnions: true,
    }
  },
  // ...other settings
};

export default config;
```

(See also https://github.com/storybookjs/storybook/issues/26496 and specifically [this comment](https://github.com/storybookjs/storybook/issues/26496#issuecomment-2051382052).)

### Before

<img width="1026" alt="Prop table in Storybook showing a union type with members printed in an arbitrary order" src="https://github.com/styleguidist/react-docgen-typescript/assets/7595652/5eb01e95-3b8f-4c80-a0bd-4bf84e130ea6">


### After

<img width="1033" alt="Prop table in Storybook showing a union type with members printed in sorted order" src="https://github.com/styleguidist/react-docgen-typescript/assets/7595652/bf3ef8b1-7358-4b9b-8a4a-cfd89198fe9e">




Patch:
diff --git a/README.md b/README.md
index a8830e2..dd5bdf0 100644
--- a/README.md
+++ b/README.md
@@ -140,6 +140,10 @@ If set to true, string enums and unions will be converted to docgen enum format.
 
 If set to true, every unions will be converted to docgen enum format.
 
+### `shouldSortUnions`: boolean
+
+When used in combination with `shouldExtractValuesFromUnion` or `shouldExtractLiteralValuesFromEnum`, sorts union members in string-sort order when set to true. This is useful for ensuring the same order of members every time.
+
 ### `skipChildrenPropWithoutDoc`: boolean (default: `true`)
 
 If set to false the docs for the `children` prop will be generated even without an explicit description.
diff --git a/src/parser.ts b/src/parser.ts
index ccf9b2f..d21f31f 100644
--- a/src/parser.ts
+++ b/src/parser.ts
@@ -87,6 +87,7 @@ export interface ParserOptions {
   shouldExtractLiteralValuesFromEnum?: boolean;
   shouldRemoveUndefinedFromOptional?: boolean;
   shouldExtractValuesFromUnion?: boolean;
+  shouldSortUnions?: boolean;
   skipChildrenPropWithoutDoc?: boolean;
   savePropValueAsString?: boolean;
   shouldIncludePropTagMap?: boolean;
@@ -220,6 +221,7 @@ export class Parser {
   private readonly shouldRemoveUndefinedFromOptional: boolean;
   private readonly shouldExtractLiteralValuesFromEnum: boolean;
   private readonly shouldExtractValuesFromUnion: boolean;
+  private readonly shouldSortUnions: boolean;
   private readonly savePropValueAsString: boolean;
   private readonly shouldIncludePropTagMap: boolean;
   private readonly shouldIncludeExpression: boolean;
@@ -230,6 +232,7 @@ export class Parser {
       shouldExtractLiteralValuesFromEnum,
       shouldRemoveUndefinedFromOptional,
       shouldExtractValuesFromUnion,
+      shouldSortUnions,
       shouldIncludePropTagMap,
       shouldIncludeExpression
     } = opts;
@@ -242,6 +245,7 @@ export class Parser {
       shouldRemoveUndefinedFromOptional
     );
     this.shouldExtractValuesFromUnion = Boolean(shouldExtractValuesFromUnion);
+    this.shouldSortUnions = Boolean(shouldSortUnions);
     this.savePropValueAsString = Boolean(savePropValueAsString);
     this.shouldIncludePropTagMap = Boolean(shouldIncludePropTagMap);
     this.shouldIncludeExpression = Boolean(shouldIncludeExpression);
@@ -635,6 +639,12 @@ export class Parser {
           value = value.filter(option => option.value != 'undefined');
         }
 
+        if (this.shouldSortUnions) {
+          value.sort((a, b) =>
+            a.value.toString().localeCompare(b.value.toString())
+          );
+        }
+
         return {
           name: 'enum',
           raw: propTypeString,


Test patch:
diff --git a/src/__tests__/data/SimpleUnions.tsx b/src/__tests__/data/SimpleUnions.tsx
new file mode 100644
index 0000000..1719497
--- /dev/null
+++ b/src/__tests__/data/SimpleUnions.tsx
@@ -0,0 +1,8 @@
+type SampleUnion = 'h1' | 'h6' | 'h2' | 'h4' | 'h5' | 'h3';
+
+interface Props {
+  /** sampleUnionProp description */
+  sampleUnionProp: SampleUnion;
+}
+
+export const SimpleUnions = (props: Props) => <div />;
diff --git a/src/__tests__/parser.ts b/src/__tests__/parser.ts
index c185147..16f9cfb 100644
--- a/src/__tests__/parser.ts
+++ b/src/__tests__/parser.ts
@@ -1448,6 +1448,63 @@ describe('parser', () => {
       });
     });
 
+    describe('Sorting unions', () => {
+      it('does not sort union members by default', () => {
+        check(
+          'SimpleUnions',
+          {
+            SimpleUnions: {
+              sampleUnionProp: {
+                raw: 'SampleUnion',
+                type: 'enum',
+                value: [
+                  { value: '"h1"' },
+                  { value: '"h6"' },
+                  { value: '"h2"' },
+                  { value: '"h4"' },
+                  { value: '"h5"' },
+                  { value: '"h3"' }
+                ]
+              }
+            }
+          },
+          false,
+          null,
+          {
+            shouldExtractLiteralValuesFromEnum: true
+          }
+        );
+      });
+
+      it('sorts union members when shouldSortUnions is true', () => {
+        check(
+          'SimpleUnions',
+          {
+            SimpleUnions: {
+              sampleUnionProp: {
+                raw: 'SampleUnion',
+                type: 'enum',
+                value: [
+                  { value: '"h1"' },
+                  { value: '"h2"' },
+                  { value: '"h3"' },
+                  { value: '"h4"' },
+                  { value: '"h5"' },
+                  { value: '"h6"' }
+                ]
+              }
+            }
+          },
+          false,
+          null,
+          {
+            shouldExtractLiteralValuesFromEnum: true,
+            shouldSortUnions: true
+          }
+        );
+      });
+    });
+
     describe('Returning not string default props ', () => {
       it('returns not string defaultProps', () => {
         check(