From cc19677b512f63b668fde8fd6252179dd799d490 Mon Sep 17 00:00:00 2001 From: Tanuj Bhaud Date: Thu, 27 Nov 2025 10:57:22 +0530 Subject: [PATCH 1/3] Fix controls displaying as object instead of select for optional union types Fixes #12641 When TypeScript properties are optional (prop?: Type), TypeScript adds | undefined to the union type. The enum detection logic was failing because undefined is not a literal type, causing the union to not be recognized as an enum. This resulted in controls displaying as "object" type instead of "select" dropdown for optional properties with union types, regardless of the number of items in the union. The fix filters out undefined elements before checking if all remaining elements are literals, allowing optional unions to be correctly recognized as enums and display as select controls. --- .../src/docs-tools/argTypes/convert/typescript/convert.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/code/core/src/docs-tools/argTypes/convert/typescript/convert.ts b/code/core/src/docs-tools/argTypes/convert/typescript/convert.ts index 3beaae0a3542..eea8e490bc60 100644 --- a/code/core/src/docs-tools/argTypes/convert/typescript/convert.ts +++ b/code/core/src/docs-tools/argTypes/convert/typescript/convert.ts @@ -43,12 +43,14 @@ export const convert = (type: TSType): SBType | void => { return { ...base, ...convertSig(type) }; case 'union': let result; - if (type.elements?.every((element) => element.name === 'literal')) { + // Filter out undefined from optional props when checking for enum + const nonUndefinedElements = type.elements?.filter((element) => element.name !== 'undefined'); + if (nonUndefinedElements?.length > 0 && nonUndefinedElements.every((element) => element.name === 'literal')) { result = { ...base, name: 'enum', // @ts-expect-error fix types - value: type.elements?.map((v) => parseLiteral(v.value)), + value: nonUndefinedElements.map((v) => parseLiteral(v.value)), }; } else { result = { ...base, name, value: type.elements?.map(convert) }; From d3b4545a32134f290bb22f9d727f0f8a9be0472b Mon Sep 17 00:00:00 2001 From: Tanuj Bhaud Date: Tue, 9 Dec 2025 01:52:07 +0530 Subject: [PATCH 2/3] Fix type errors and linting issues in optional union enum handling - Add type guard functions (isLiteral, isUndefined) for better type narrowing - Add 'undefined' to TSScalarType union in types.ts - Make elements non-optional in TSCombinationType (unions always have elements) - Wrap case block in braces to fix Biome linting error - Use proper type assertion instead of @ts-expect-error --- .../argTypes/convert/typescript/convert.ts | 30 +++++++++++-------- .../argTypes/convert/typescript/types.ts | 4 +-- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/code/core/src/docs-tools/argTypes/convert/typescript/convert.ts b/code/core/src/docs-tools/argTypes/convert/typescript/convert.ts index eea8e490bc60..2a986ef90efd 100644 --- a/code/core/src/docs-tools/argTypes/convert/typescript/convert.ts +++ b/code/core/src/docs-tools/argTypes/convert/typescript/convert.ts @@ -4,6 +4,10 @@ import type { SBType } from 'storybook/internal/types'; import { parseLiteral } from '../utils'; import type { TSSigType, TSType } from './types'; +// Type guards for narrowing TSType discriminant unions +const isLiteral = (type: TSType): boolean => type.name === 'literal'; +const isUndefined = (type: TSType): boolean => type.name === 'undefined'; + const convertSig = (type: TSSigType) => { switch (type.type) { case 'function': @@ -41,23 +45,25 @@ export const convert = (type: TSType): SBType | void => { } case 'signature': return { ...base, ...convertSig(type) }; - case 'union': - let result; - // Filter out undefined from optional props when checking for enum - const nonUndefinedElements = type.elements?.filter((element) => element.name !== 'undefined'); - if (nonUndefinedElements?.length > 0 && nonUndefinedElements.every((element) => element.name === 'literal')) { - result = { + case 'union': { + const nonUndefinedElements = type.elements.filter((element) => !isUndefined(element)); + const allLiterals = nonUndefinedElements.length > 0 && nonUndefinedElements.every(isLiteral); + + if (allLiterals) { + return { ...base, name: 'enum', - // @ts-expect-error fix types - value: nonUndefinedElements.map((v) => parseLiteral(v.value)), + value: nonUndefinedElements.map((element) => { + // We know element is a literal type because of the allLiterals check + const literalElement = element as Extract; + return parseLiteral(literalElement.value); + }), }; - } else { - result = { ...base, name, value: type.elements?.map(convert) }; } - return result; + return { ...base, name, value: type.elements.map(convert) }; + } case 'intersection': - return { ...base, name, value: type.elements?.map(convert) }; + return { ...base, name, value: type.elements.map(convert) }; default: return { ...base, name: 'other', value: name }; } diff --git a/code/core/src/docs-tools/argTypes/convert/typescript/types.ts b/code/core/src/docs-tools/argTypes/convert/typescript/types.ts index 79df2e812c00..3c3516d16e8c 100644 --- a/code/core/src/docs-tools/argTypes/convert/typescript/types.ts +++ b/code/core/src/docs-tools/argTypes/convert/typescript/types.ts @@ -9,7 +9,7 @@ type TSArgType = TSType; type TSCombinationType = TSBaseType & { name: 'union' | 'intersection'; - elements?: TSType[]; + elements: TSType[]; }; type TSFuncSigType = TSBaseType & { @@ -33,7 +33,7 @@ type TSObjectSigType = TSBaseType & { }; type TSScalarType = TSBaseType & { - name: 'any' | 'boolean' | 'number' | 'void' | 'string' | 'symbol' | 'literal'; + name: 'any' | 'boolean' | 'number' | 'void' | 'string' | 'symbol' | 'literal' | 'undefined'; }; type TSArrayType = TSBaseType & { From d837343b2b41f45c8b2256548b87775ac04a3f4a Mon Sep 17 00:00:00 2001 From: Tanuj Bhaud Date: Tue, 9 Dec 2025 02:16:36 +0530 Subject: [PATCH 3/3] Fix TypeScript type errors: add TSLiteralType with value property - Separate TSLiteralType from TSScalarType to include 'value' property - Use proper TypeScript type guard syntax (type is X) for narrowing - Filter with type guard to get properly typed literal elements - Removes need for explicit type casting --- .../argTypes/convert/typescript/convert.ts | 15 ++++++++------- .../argTypes/convert/typescript/types.ts | 9 +++++++-- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/code/core/src/docs-tools/argTypes/convert/typescript/convert.ts b/code/core/src/docs-tools/argTypes/convert/typescript/convert.ts index 2a986ef90efd..c66504aee88a 100644 --- a/code/core/src/docs-tools/argTypes/convert/typescript/convert.ts +++ b/code/core/src/docs-tools/argTypes/convert/typescript/convert.ts @@ -5,8 +5,11 @@ import { parseLiteral } from '../utils'; import type { TSSigType, TSType } from './types'; // Type guards for narrowing TSType discriminant unions -const isLiteral = (type: TSType): boolean => type.name === 'literal'; -const isUndefined = (type: TSType): boolean => type.name === 'undefined'; +type TSLiteralType = Extract; +type TSUndefinedType = Extract; + +const isLiteral = (type: TSType): type is TSLiteralType => type.name === 'literal'; +const isUndefined = (type: TSType): type is TSUndefinedType => type.name === 'undefined'; const convertSig = (type: TSSigType) => { switch (type.type) { @@ -50,14 +53,12 @@ export const convert = (type: TSType): SBType | void => { const allLiterals = nonUndefinedElements.length > 0 && nonUndefinedElements.every(isLiteral); if (allLiterals) { + // TypeScript can't infer from .every(), so we filter again with the type guard + const literalElements = nonUndefinedElements.filter(isLiteral); return { ...base, name: 'enum', - value: nonUndefinedElements.map((element) => { - // We know element is a literal type because of the allLiterals check - const literalElement = element as Extract; - return parseLiteral(literalElement.value); - }), + value: literalElements.map((element) => parseLiteral(element.value)), }; } return { ...base, name, value: type.elements.map(convert) }; diff --git a/code/core/src/docs-tools/argTypes/convert/typescript/types.ts b/code/core/src/docs-tools/argTypes/convert/typescript/types.ts index 3c3516d16e8c..ba4f02a090b8 100644 --- a/code/core/src/docs-tools/argTypes/convert/typescript/types.ts +++ b/code/core/src/docs-tools/argTypes/convert/typescript/types.ts @@ -33,7 +33,12 @@ type TSObjectSigType = TSBaseType & { }; type TSScalarType = TSBaseType & { - name: 'any' | 'boolean' | 'number' | 'void' | 'string' | 'symbol' | 'literal' | 'undefined'; + name: 'any' | 'boolean' | 'number' | 'void' | 'string' | 'symbol' | 'undefined'; +}; + +type TSLiteralType = TSBaseType & { + name: 'literal'; + value: string; }; type TSArrayType = TSBaseType & { @@ -43,4 +48,4 @@ type TSArrayType = TSBaseType & { export type TSSigType = TSObjectSigType | TSFuncSigType; -export type TSType = TSScalarType | TSCombinationType | TSSigType | TSArrayType; +export type TSType = TSScalarType | TSLiteralType | TSCombinationType | TSSigType | TSArrayType;