Skip to content

Angular: Fix single-quoted union types in compodoc control inference#34904

Closed
Giazz73-Desavoie wants to merge 1 commit into
storybookjs:nextfrom
Giazz73-Desavoie:fix/angular-single-quoted-union-controls
Closed

Angular: Fix single-quoted union types in compodoc control inference#34904
Giazz73-Desavoie wants to merge 1 commit into
storybookjs:nextfrom
Giazz73-Desavoie:fix/angular-single-quoted-union-controls

Conversation

@Giazz73-Desavoie
Copy link
Copy Markdown

@Giazz73-Desavoie Giazz73-Desavoie commented May 26, 2026

What changed

extractEnumValues in code/frameworks/angular/src/client/compodoc.ts now handles TypeScript literal union types that Compodoc emits with single-quoted strings (e.g. 'S' | 'M' | 'L').

Previously, JSON.parse("'S'") would throw, causing the entire union to be treated as an opaque type and fall back to an object control. Users had to manually declare options in their stories to get a select/radio control — defeating the purpose of automatic control inference.

Root cause

Compodoc serialises Angular input<'S' | 'M' | 'L'>() as the raw TypeScript string 'S' | 'M' | 'L' (single-quoted), while JSON.parse only accepts double-quoted strings. The catch branch silently returned null, hiding the inference failure.

Fix

Parse each pipe-delimited segment individually:

  1. If the segment is surrounded by single quotes, strip them and unescape \' sequences.
  2. Otherwise fall back to JSON.parse (handles double-quoted strings, numbers, booleans, null).
  3. If any segment cannot be parsed, return null for the whole union — preserving the existing behaviour for opaque or complex types.

Also tightened the enum-child guard from x.value (truthiness) to x.value != null, so enum values like "" are included correctly.

Tests

Added two test cases to the existing extractType table in compodoc.test.ts:

  • "'primary' | 'secondary'"{ name: 'enum', value: ['primary', 'secondary'] }
  • "'S' | 'M' | 'L'"{ name: 'enum', value: ['S', 'M', 'L'] }

The existing '"primary" | "secondary"' case continues to pass unchanged.

Manual testing

  1. Create an Angular component with a single-quoted string union input:
    @Component({ ... })
    export class ButtonComponent {
      size = input<'S' | 'M' | 'L'>('M');
    }
  2. Run Storybook with Compodoc (--docs or equivalent).
  3. Open the story — the size arg should display as a select control with options S, M, L instead of an empty object control.

Fixes #33779

Summary by CodeRabbit

  • Bug Fixes

    • Improved robustness of enum value parsing for TypeScript union types, including better handling of quoted string literals and null/undefined values.
  • Tests

    • Expanded test coverage for enum type extraction with additional string-union scenarios.

Review Change Stack

…ference

Compodoc emits TypeScript literal union types with single-quoted strings
(e.g. 'S' | 'M' | 'L') which JSON.parse rejects, causing the union to
fall back to an object control instead of the expected select/radio control.

Parse each pipe-delimited segment individually: strip surrounding single
quotes and unescape inner escaped quotes before falling back to JSON.parse
for double-quoted strings, numbers, and booleans. Return null for the whole
union if any segment cannot be parsed, preserving existing behaviour for
opaque types.

Also tighten the enum-child guard from truthiness (x.value) to
x.value != null so enum values like "" are correctly included.

Fixes storybookjs#33779
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 26, 2026

Fails
🚫

PR is not labeled with one of: ["cleanup","BREAKING CHANGE","feature request","bug","documentation","maintenance","build","dependencies"]

🚫

PR is not labeled with one of: ["ci:normal","ci:merged","ci:daily","ci:docs"]

Generated by 🚫 dangerJS against f2a9fd2

@Giazz73-Desavoie Giazz73-Desavoie changed the title fix(angular): handle single-quoted union types in compodoc control inference Angular: Fix single-quoted union types in compodoc control inference May 26, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 26, 2026

📝 Walkthrough

Walkthrough

The PR enhances extractEnumValues in the Angular Compodoc framework integration to robustly parse TypeScript string literal unions emitted with single quotes. The implementation now handles quote stripping, unescaping, and per-segment validation, with test cases validating enum extraction for quoted string unions.

Changes

Angular Compodoc Union Enum Extraction

Layer / File(s) Summary
Enhanced enum value extraction from unions
code/frameworks/angular/src/client/compodoc.ts
extractEnumValues tightens the child-values readiness check to require non-null/non-undefined values, adds support for single-quoted literal unions by stripping surrounding quotes and unescaping \', and performs per-segment JSON.parse with early return on parse failure.
Test coverage for union enum parsing
code/frameworks/angular/src/client/compodoc.test.ts
Parameterized test cases are added to extractType to verify enum detection for quoted string unions, including mixed-case ('primary' | 'secondary') and single-character ('S' | 'M' | 'L') unions.

🎯 2 (Simple) | ⏱️ ~10 minutes


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@code/frameworks/angular/src/client/compodoc.ts`:
- Around line 133-147: The single-quoted branch in the compodocType parser
(inside the values mapping) only unescapes \', but must decode all JavaScript
escape sequences; replace the simple replace with logic that takes the inner
content (s.slice(1, -1)), safely constructs a JSON string literal by escaping
backslashes and double quotes, then call JSON.parse on '"' + innerEscaped + '"'
to decode sequences (so use JSON.parse to produce the final decoded string
instead of only replacing /\\'/g), returning that decoded value in the values
array and keeping the existing fallback to JSON.parse for non-quoted segments;
update the code where compodocType and values are handled to use this decoding
path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7642f1bb-880c-42b8-af33-6f9863c1d98a

📥 Commits

Reviewing files that changed from the base of the PR and between 11e2a8c and f2a9fd2.

📒 Files selected for processing (2)
  • code/frameworks/angular/src/client/compodoc.test.ts
  • code/frameworks/angular/src/client/compodoc.ts

Comment on lines +133 to +147
const values = compodocType.split('|').map((segment): unknown => {
const s = segment.trim();
// Compodoc emits TypeScript literal unions with single-quoted strings (e.g. 'S' | 'M' | 'L'),
// which JSON.parse rejects. Strip the surrounding quotes and unescape inner escaped quotes.
if (s.length >= 2 && s[0] === "'" && s[s.length - 1] === "'") {
return s.slice(1, -1).replace(/\\'/g, "'");
}
try {
return JSON.parse(s);
} catch {
return undefined;
}
});

return values.some((v) => v === undefined) ? null : values;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In JavaScript/TypeScript single-quoted string literals, are escape sequences such as \n, \t, \\, \", \', and \uXXXX interpreted the same as in double-quoted strings, and what runtime string values do they produce?

💡 Result:

In JavaScript and TypeScript, single-quoted ('...') and double-quoted ("...") string literals are semantically identical regarding escape sequences [1][2]. All standard escape sequences—including \n, \t, \, ", ', and \uXXXX—are interpreted and processed in exactly the same way regardless of whether the string is wrapped in single or double quotes [3][4][2]. When these escape sequences are used, they produce the following runtime string values: \n: Line Feed (LF) character (U+000A) [3][4] \t: Character Tabulation (horizontal tab) (U+0009) [3][4] \: Backslash (Reverse Solidus) (U+005C) [3][4] ": Quotation Mark (U+0022) [3][4] ': Apostrophe (U+0027) [3][4] \uXXXX: A character represented by the hexadecimal Unicode code point XXXX (e.g., \u0041 produces 'A') [3][5] Because both quote types are treated identically, the only functional difference is which character must be escaped to avoid prematurely terminating the string: you must escape the quote character used to define the literal [4][2]. For example, in a single-quoted string, a literal single quote must be escaped as ', while a double quote can appear literally [2]. Conversely, in a double-quoted string, a literal double quote must be escaped as ", while a single quote can appear literally [2]. All other escape sequences remain fully interchangeable [2].

Citations:


Decode all valid escape sequences in the single-quoted enum segment parser (lines 133-147 in code/frameworks/angular/src/client/compodoc.ts)
The current single-quoted branch only unescapes \' (s.slice(1, -1).replace(/\\'/g, "'")) and leaves other valid TypeScript/JavaScript string escapes (e.g. \n, \t, \\, \", \uXXXX) undecoded, so inferred enum option values can diverge from the actual runtime strings.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code/frameworks/angular/src/client/compodoc.ts` around lines 133 - 147, The
single-quoted branch in the compodocType parser (inside the values mapping) only
unescapes \', but must decode all JavaScript escape sequences; replace the
simple replace with logic that takes the inner content (s.slice(1, -1)), safely
constructs a JSON string literal by escaping backslashes and double quotes, then
call JSON.parse on '"' + innerEscaped + '"' to decode sequences (so use
JSON.parse to produce the final decoded string instead of only replacing
/\\'/g), returning that decoded value in the values array and keeping the
existing fallback to JSON.parse for non-quoted segments; update the code where
compodocType and values are handled to use this decoding path.

@valentinpalkovic
Copy link
Copy Markdown
Contributor

Superseded by #34887

@Giazz73-Desavoie
Copy link
Copy Markdown
Author

Updated the single-quoted segment parser to use JSON.parse for proper escape-sequence decoding (addresses the CodeRabbit review).

Regarding the "superseded by #34887" note: our approach differs in one important way — we return null for the whole union when any segment cannot be parsed, preserving the existing fallback to object for opaque/complex types (e.g. boolean | string | MyClass). PR #34887 instead falls through to returning the raw literal string for unrecognised segments, which would silently convert boolean | string into enum options ['boolean', 'string'] — a different kind of regression.

We also include two test cases in compodoc.test.ts covering the new behaviour.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[Bug]: Storybook 10 no longer infers controls from Typescript literal union types

3 participants