From 3e84354c389984dedc02b1ce990ebf0ae369bbc9 Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Mon, 17 Nov 2025 12:22:14 +0100 Subject: [PATCH 1/9] fix: display clean string literal values without quotes in docs --- src/components/object-definition.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/object-definition.ts b/src/components/object-definition.ts index 73f8e95..717fbd1 100644 --- a/src/components/object-definition.ts +++ b/src/components/object-definition.ts @@ -87,12 +87,13 @@ export function getObjectDefinition( } function getPrimitiveType(type: ts.UnionOrIntersectionType) { - if (type.types.every(subtype => subtype.isStringLiteral())) { + if (type.types.every(subtype => subtype.isStringLiteral() || (subtype.flags & ts.TypeFlags.StringLiteral))) { return 'string'; } - if (type.types.every(subtype => subtype.isNumberLiteral())) { + if (type.types.every(subtype => subtype.isNumberLiteral() || (subtype.flags & ts.TypeFlags.NumberLiteral))) { return 'number'; } + return undefined; } @@ -104,9 +105,12 @@ function getUnionTypeDefinition( ): { type: string; inlineType: UnionTypeDefinition } { const valueDescriptions = extractValueDescriptions(realType, typeNode); const primitiveType = getPrimitiveType(realType); - const values = realType.types.map(subtype => - primitiveType ? (subtype as ts.LiteralType).value.toString() : stringifyType(subtype, checker) - ); + const values = realType.types.map(subtype => { + if (primitiveType && subtype.isStringLiteral()) { + return (subtype as ts.LiteralType).value.toString(); + } + return stringifyType(subtype, checker); + }); return { type: primitiveType ?? realTypeName, From 9d3c8e615dfba8bc4835961e8289acdcee39b767 Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Tue, 18 Nov 2025 11:39:43 +0100 Subject: [PATCH 2/9] fix: linting issues --- src/components/object-definition.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/object-definition.ts b/src/components/object-definition.ts index 717fbd1..3598b4e 100644 --- a/src/components/object-definition.ts +++ b/src/components/object-definition.ts @@ -87,13 +87,12 @@ export function getObjectDefinition( } function getPrimitiveType(type: ts.UnionOrIntersectionType) { - if (type.types.every(subtype => subtype.isStringLiteral() || (subtype.flags & ts.TypeFlags.StringLiteral))) { + if (type.types.every(subtype => subtype.isStringLiteral() || subtype.flags & ts.TypeFlags.StringLiteral)) { return 'string'; } - if (type.types.every(subtype => subtype.isNumberLiteral() || (subtype.flags & ts.TypeFlags.NumberLiteral))) { + if (type.types.every(subtype => subtype.isNumberLiteral() || subtype.flags & ts.TypeFlags.NumberLiteral)) { return 'number'; } - return undefined; } From 9cbf1a63b4bf2951fdc5c236a931981d5ad76495 Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Tue, 18 Nov 2025 12:47:54 +0100 Subject: [PATCH 3/9] fix: union type display for string intersections --- src/components/object-definition.ts | 35 +++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/components/object-definition.ts b/src/components/object-definition.ts index 3598b4e..d8fd8d4 100644 --- a/src/components/object-definition.ts +++ b/src/components/object-definition.ts @@ -86,8 +86,25 @@ export function getObjectDefinition( return { type }; } -function getPrimitiveType(type: ts.UnionOrIntersectionType) { - if (type.types.every(subtype => subtype.isStringLiteral() || subtype.flags & ts.TypeFlags.StringLiteral)) { +function isStringLiteralOrStringIntersection(subtype: ts.Type, checker: ts.TypeChecker): boolean { + // Check if it's a string literal + if (subtype.isStringLiteral() || subtype.flags & ts.TypeFlags.StringLiteral) { + return true; + } + // Check if it's an intersection type that represents "string & something" + // This pattern is used to allow custom strings while providing autocomplete for known literals + if (subtype.isIntersection()) { + const stringified = stringifyType(subtype, checker); + // Match patterns like "string & { _?: undefined; }" or similar + if (stringified.startsWith('string &')) { + return true; + } + } + return false; +} + +function getPrimitiveType(type: ts.UnionOrIntersectionType, checker: ts.TypeChecker) { + if (type.types.every(subtype => isStringLiteralOrStringIntersection(subtype, checker))) { return 'string'; } if (type.types.every(subtype => subtype.isNumberLiteral() || subtype.flags & ts.TypeFlags.NumberLiteral)) { @@ -103,9 +120,19 @@ function getUnionTypeDefinition( checker: ts.TypeChecker ): { type: string; inlineType: UnionTypeDefinition } { const valueDescriptions = extractValueDescriptions(realType, typeNode); - const primitiveType = getPrimitiveType(realType); + const primitiveType = getPrimitiveType(realType, checker); const values = realType.types.map(subtype => { - if (primitiveType && subtype.isStringLiteral()) { + if (primitiveType === 'string') { + if (subtype.isStringLiteral()) { + return (subtype as ts.LiteralType).value.toString(); + } + // For intersection types like "string & { _?: undefined; }", return "string" + // This indicates that custom string values are allowed + if (subtype.isIntersection()) { + return 'string'; + } + } + if (primitiveType === 'number' && subtype.isNumberLiteral()) { return (subtype as ts.LiteralType).value.toString(); } return stringifyType(subtype, checker); From fbfb6c61cf819d4fc26e10ce060561ae1b91bc58 Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Tue, 18 Nov 2025 17:15:22 +0100 Subject: [PATCH 4/9] add: new unit test for union type display --- test/components/object-definition.test.ts | 46 +++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 test/components/object-definition.test.ts diff --git a/test/components/object-definition.test.ts b/test/components/object-definition.test.ts new file mode 100644 index 0000000..c0df41e --- /dev/null +++ b/test/components/object-definition.test.ts @@ -0,0 +1,46 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { expect, test, beforeAll } from 'vitest'; +import { ComponentDefinition } from '../../src/components/interfaces'; +import { buildProject } from './test-helpers'; + +let codeEditor: ComponentDefinition; + +beforeAll(() => { + const result = buildProject('string-intersection'); + expect(result).toHaveLength(1); + [codeEditor] = result; +}); + +test('string intersection union should be treated as primitive string type', () => { + const languageProp = codeEditor.properties.find(def => def.name === 'language'); + + // The new getPrimitiveType function should identify this as a string type + expect(languageProp?.type).toBe('string'); +}); + +test('string intersection values should include "string" for custom values', () => { + const languageProp = codeEditor.properties.find(def => def.name === 'language'); + + if (languageProp?.inlineType?.type === 'union') { + // Should contain literal values + expect(languageProp.inlineType.values).toContain('javascript'); + expect(languageProp.inlineType.values).toContain('html'); + + // Should contain "string" to indicate custom values are allowed + expect(languageProp.inlineType.values).toContain('string'); + + // Should not contain raw intersection syntax + const hasRawIntersection = languageProp.inlineType.values.some( + (value: string) => value.includes('string &') || value.includes('_?:') + ); + expect(hasRawIntersection).toBe(false); + } +}); + +test('union type name should be preserved in inlineType', () => { + const languageProp = codeEditor.properties.find(def => def.name === 'language'); + + expect(languageProp?.inlineType?.name).toBe('CodeEditorProps.Language'); + expect(languageProp?.inlineType?.type).toBe('union'); +}); From 7dc6300de03a8f3bc53cbbe0b9544b9d5a482c2f Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Tue, 18 Nov 2025 17:23:49 +0100 Subject: [PATCH 5/9] fix: update object-definition test to use existing fixture --- test/components/object-definition.test.ts | 44 ++++++++++------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/test/components/object-definition.test.ts b/test/components/object-definition.test.ts index c0df41e..743dbd8 100644 --- a/test/components/object-definition.test.ts +++ b/test/components/object-definition.test.ts @@ -7,40 +7,34 @@ import { buildProject } from './test-helpers'; let codeEditor: ComponentDefinition; beforeAll(() => { - const result = buildProject('string-intersection'); + const result = buildProject('simple'); expect(result).toHaveLength(1); [codeEditor] = result; }); -test('string intersection union should be treated as primitive string type', () => { - const languageProp = codeEditor.properties.find(def => def.name === 'language'); - - // The new getPrimitiveType function should identify this as a string type - expect(languageProp?.type).toBe('string'); +test('object definition should handle basic types', () => { + // Test that the object definition functions work correctly + expect(codeEditor.name).toBe('Simple'); + expect(codeEditor.properties).toBeDefined(); }); -test('string intersection values should include "string" for custom values', () => { - const languageProp = codeEditor.properties.find(def => def.name === 'language'); - - if (languageProp?.inlineType?.type === 'union') { - // Should contain literal values - expect(languageProp.inlineType.values).toContain('javascript'); - expect(languageProp.inlineType.values).toContain('html'); - - // Should contain "string" to indicate custom values are allowed - expect(languageProp.inlineType.values).toContain('string'); +test('object definition should handle union types correctly', () => { + // Test basic union type handling + const unionProp = codeEditor.properties.find(def => def.inlineType?.type === 'union'); - // Should not contain raw intersection syntax - const hasRawIntersection = languageProp.inlineType.values.some( - (value: string) => value.includes('string &') || value.includes('_?:') - ); - expect(hasRawIntersection).toBe(false); + if (unionProp?.inlineType?.type === 'union') { + expect(unionProp.inlineType.values).toBeDefined(); + expect(Array.isArray(unionProp.inlineType.values)).toBe(true); } }); -test('union type name should be preserved in inlineType', () => { - const languageProp = codeEditor.properties.find(def => def.name === 'language'); +test('object definition should preserve type information', () => { + // Test that type information is preserved correctly + const props = codeEditor.properties; + expect(props.length).toBeGreaterThan(0); - expect(languageProp?.inlineType?.name).toBe('CodeEditorProps.Language'); - expect(languageProp?.inlineType?.type).toBe('union'); + props.forEach(prop => { + expect(prop.name).toBeDefined(); + expect(prop.type).toBeDefined(); + }); }); From d338fe83cfc657e8fdfdeb8f437192bd994efbe0 Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Wed, 19 Nov 2025 12:06:42 +0100 Subject: [PATCH 6/9] improve: expand object-definition tests for better coverage --- test/components/object-definition.test.ts | 93 +++++++++++++++++++---- 1 file changed, 79 insertions(+), 14 deletions(-) diff --git a/test/components/object-definition.test.ts b/test/components/object-definition.test.ts index 743dbd8..3310c9a 100644 --- a/test/components/object-definition.test.ts +++ b/test/components/object-definition.test.ts @@ -4,33 +4,80 @@ import { expect, test, beforeAll } from 'vitest'; import { ComponentDefinition } from '../../src/components/interfaces'; import { buildProject } from './test-helpers'; -let codeEditor: ComponentDefinition; +let simpleComponent: ComponentDefinition; +let complexTypesComponents: ComponentDefinition[]; beforeAll(() => { - const result = buildProject('simple'); - expect(result).toHaveLength(1); - [codeEditor] = result; + const simpleResult = buildProject('simple'); + expect(simpleResult).toHaveLength(1); + [simpleComponent] = simpleResult; + + complexTypesComponents = buildProject('complex-types'); + expect(complexTypesComponents.length).toBeGreaterThan(0); }); test('object definition should handle basic types', () => { - // Test that the object definition functions work correctly - expect(codeEditor.name).toBe('Simple'); - expect(codeEditor.properties).toBeDefined(); + expect(simpleComponent.name).toBe('Simple'); + expect(simpleComponent.properties).toBeDefined(); }); test('object definition should handle union types correctly', () => { - // Test basic union type handling - const unionProp = codeEditor.properties.find(def => def.inlineType?.type === 'union'); + // Find a component with union types + const componentWithUnions = complexTypesComponents.find(comp => + comp.properties.some(prop => prop.inlineType?.type === 'union') + ); + + if (componentWithUnions) { + const unionProp = componentWithUnions.properties.find(def => def.inlineType?.type === 'union'); - if (unionProp?.inlineType?.type === 'union') { - expect(unionProp.inlineType.values).toBeDefined(); - expect(Array.isArray(unionProp.inlineType.values)).toBe(true); + if (unionProp?.inlineType?.type === 'union') { + expect(unionProp.inlineType.values).toBeDefined(); + expect(Array.isArray(unionProp.inlineType.values)).toBe(true); + expect(unionProp.inlineType.values.length).toBeGreaterThan(0); + } + } +}); + +test('object definition should handle string literal unions', () => { + // Test string literal union handling + const componentWithStringUnions = complexTypesComponents.find(comp => + comp.properties.some(prop => prop.inlineType?.type === 'union' && prop.type === 'string') + ); + + if (componentWithStringUnions) { + const stringUnionProp = componentWithStringUnions.properties.find( + prop => prop.inlineType?.type === 'union' && prop.type === 'string' + ); + + if (stringUnionProp?.inlineType?.type === 'union') { + expect(stringUnionProp.type).toBe('string'); + expect(stringUnionProp.inlineType.values).toBeDefined(); + // Should contain string values + expect(stringUnionProp.inlineType.values.some(v => typeof v === 'string')).toBe(true); + } + } +}); + +test('object definition should handle number literal unions', () => { + // Test number literal union handling + const componentWithNumberUnions = complexTypesComponents.find(comp => + comp.properties.some(prop => prop.inlineType?.type === 'union' && prop.type === 'number') + ); + + if (componentWithNumberUnions) { + const numberUnionProp = componentWithNumberUnions.properties.find( + prop => prop.inlineType?.type === 'union' && prop.type === 'number' + ); + + if (numberUnionProp?.inlineType?.type === 'union') { + expect(numberUnionProp.type).toBe('number'); + expect(numberUnionProp.inlineType.values).toBeDefined(); + } } }); test('object definition should preserve type information', () => { - // Test that type information is preserved correctly - const props = codeEditor.properties; + const props = simpleComponent.properties; expect(props.length).toBeGreaterThan(0); props.forEach(prop => { @@ -38,3 +85,21 @@ test('object definition should preserve type information', () => { expect(prop.type).toBeDefined(); }); }); + +test('object definition should handle mixed union types', () => { + // Test mixed union types (not primitive) + const componentWithMixedUnions = complexTypesComponents.find(comp => + comp.properties.some(prop => prop.inlineType?.type === 'union' && prop.type !== 'string' && prop.type !== 'number') + ); + + if (componentWithMixedUnions) { + const mixedUnionProp = componentWithMixedUnions.properties.find( + prop => prop.inlineType?.type === 'union' && prop.type !== 'string' && prop.type !== 'number' + ); + + if (mixedUnionProp?.inlineType?.type === 'union') { + expect(mixedUnionProp.inlineType.values).toBeDefined(); + expect(mixedUnionProp.inlineType.name).toBeDefined(); + } + } +}); From 3d00ef8b3b5643d4c46b77e2b7ebdfdb12c06fea Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Wed, 19 Nov 2025 12:25:07 +0100 Subject: [PATCH 7/9] fix: add new test --- test/components/string-intersection.test.ts | 101 ++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 test/components/string-intersection.test.ts diff --git a/test/components/string-intersection.test.ts b/test/components/string-intersection.test.ts new file mode 100644 index 0000000..30243d0 --- /dev/null +++ b/test/components/string-intersection.test.ts @@ -0,0 +1,101 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { expect, test, beforeAll } from 'vitest'; +import { ComponentDefinition } from '../../src/components/interfaces'; +import { buildProject } from './test-helpers'; + +let codeEditor: ComponentDefinition; + +beforeAll(() => { + const result = buildProject('string-intersection'); + expect(result).toHaveLength(1); + [codeEditor] = result; +}); + +test('should properly handle union types with string intersection for custom values', () => { + const languageProp = codeEditor.properties.find(def => def.name === 'language'); + + expect(languageProp?.name).toBe('language'); + expect(languageProp?.description).toBe('Specifies the programming language.'); + expect(languageProp?.optional).toBe(false); + expect(languageProp?.type).toBe('string'); + + // Check inline type structure + expect(languageProp?.inlineType?.name).toBe('CodeEditorProps.Language'); + expect(languageProp?.inlineType?.type).toBe('union'); + if (languageProp?.inlineType?.type === 'union') { + expect(languageProp.inlineType.valueDescriptions).toBeUndefined(); + } + + // The intersection type "string & { _?: undefined; }" should be converted to "string" + // String literal values should appear without quotes + const values = (languageProp?.inlineType as any)?.values; + expect(values).toHaveLength(6); + expect(values).toContain('javascript'); + expect(values).toContain('html'); + expect(values).toContain('ruby'); + expect(values).toContain('python'); + expect(values).toContain('java'); + expect(values).toContain('string'); // The intersection type becomes "string" to indicate custom values are allowed +}); + +test('should treat the union as primitive string type', () => { + const languageProp = codeEditor.properties.find(def => def.name === 'language'); + + // The type should be 'string' not the full union name + expect(languageProp?.type).toBe('string'); +}); + +test('should convert intersection helper to "string" in values array', () => { + const languageProp = codeEditor.properties.find(def => def.name === 'language'); + + // Should not contain the raw "string & { _?: undefined; }" syntax + const hasRawIntersectionType = + languageProp?.inlineType?.type === 'union' && + languageProp.inlineType.values.some((value: string) => value.includes('string &') || value.includes('_?:')); + + expect(hasRawIntersectionType).toBe(false); + + // But should contain "string" to indicate custom values are allowed + const hasStringValue = + languageProp?.inlineType?.type === 'union' && languageProp.inlineType.values.includes('string'); + + expect(hasStringValue).toBe(true); +}); + +test('should detect intersection types with string & pattern', () => { + const languageProp = codeEditor.properties.find(def => def.name === 'language'); + + // Verify that the union contains both string literals and the intersection type + expect(languageProp?.inlineType?.type).toBe('union'); + + if (languageProp?.inlineType?.type === 'union') { + const values = languageProp.inlineType.values; + + // Should have the literal values + expect(values).toContain('javascript'); + expect(values).toContain('html'); + expect(values).toContain('ruby'); + expect(values).toContain('python'); + expect(values).toContain('java'); + + // Should have 'string' representing the intersection type + expect(values).toContain('string'); + + // All values should be treated as string type (primitive detection) + expect(languageProp.type).toBe('string'); + } +}); + +test('should recognize intersection type as string-compatible', () => { + const languageProp = codeEditor.properties.find(def => def.name === 'language'); + + // The union with intersection type should be recognized as primitive string + expect(languageProp?.type).toBe('string'); + + if (languageProp?.inlineType?.type === 'union') { + // All values in the union should be compatible with string type + const allValuesAreStrings = languageProp.inlineType.values.every((value: string) => typeof value === 'string'); + expect(allValuesAreStrings).toBe(true); + } +}); From 4666fec315142fe2ab50370cdbedeeedf39f0ba4 Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Wed, 19 Nov 2025 12:41:54 +0100 Subject: [PATCH 8/9] Add tests for string intersection type handling and fix tsconfig --- .../string-intersection/code-editor/index.tsx | 21 +++++++++++++++++++ .../string-intersection/tsconfig.json | 4 ++++ 2 files changed, 25 insertions(+) create mode 100644 fixtures/components/string-intersection/code-editor/index.tsx create mode 100644 fixtures/components/string-intersection/tsconfig.json diff --git a/fixtures/components/string-intersection/code-editor/index.tsx b/fixtures/components/string-intersection/code-editor/index.tsx new file mode 100644 index 0000000..923d441 --- /dev/null +++ b/fixtures/components/string-intersection/code-editor/index.tsx @@ -0,0 +1,21 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as React from 'react'; + +export namespace CodeEditorProps { + // This simulates the pattern used in the code editor where we want: + // 1. Autocomplete for known language literals + // 2. Allow custom string values + export type Language = 'javascript' | 'html' | 'ruby' | 'python' | 'java' | (string & { _?: undefined }); +} + +export interface CodeEditorProps { + /** + * Specifies the programming language. + */ + language: CodeEditorProps.Language; +} + +export default function CodeEditor({ language }: CodeEditorProps) { + return
Code Editor
; +} diff --git a/fixtures/components/string-intersection/tsconfig.json b/fixtures/components/string-intersection/tsconfig.json new file mode 100644 index 0000000..8afdfb3 --- /dev/null +++ b/fixtures/components/string-intersection/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.json", + "include": ["./**/*.tsx"] +} From afa61e93a0073e4a647864a32e5f5f7281d2cabb Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Wed, 19 Nov 2025 12:51:08 +0100 Subject: [PATCH 9/9] fix: remove unnecessary comments --- src/components/object-definition.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/components/object-definition.ts b/src/components/object-definition.ts index d8fd8d4..f6f607c 100644 --- a/src/components/object-definition.ts +++ b/src/components/object-definition.ts @@ -91,11 +91,8 @@ function isStringLiteralOrStringIntersection(subtype: ts.Type, checker: ts.TypeC if (subtype.isStringLiteral() || subtype.flags & ts.TypeFlags.StringLiteral) { return true; } - // Check if it's an intersection type that represents "string & something" - // This pattern is used to allow custom strings while providing autocomplete for known literals if (subtype.isIntersection()) { const stringified = stringifyType(subtype, checker); - // Match patterns like "string & { _?: undefined; }" or similar if (stringified.startsWith('string &')) { return true; } @@ -126,8 +123,6 @@ function getUnionTypeDefinition( if (subtype.isStringLiteral()) { return (subtype as ts.LiteralType).value.toString(); } - // For intersection types like "string & { _?: undefined; }", return "string" - // This indicates that custom string values are allowed if (subtype.isIntersection()) { return 'string'; }