@@ -54,6 +54,8 @@ export class TypeProcessor {
5454 this . emittedEnumNames = new Set ( ) ;
5555 /** @type {Set<string> } */
5656 this . emittedStructuredTypeNames = new Set ( ) ;
57+ /** @type {Set<string> } */
58+ this . emittedStringLiteralUnionNames = new Set ( ) ;
5759
5860 /** @type {Set<string> } */
5961 this . visitedDeclarationKeys = new Set ( ) ;
@@ -145,6 +147,11 @@ export class TypeProcessor {
145147
146148 for ( const [ type , node ] of this . seenTypes ) {
147149 this . seenTypes . delete ( type ) ;
150+ const stringLiteralUnion = this . getStringLiteralUnionLiterals ( type ) ;
151+ if ( stringLiteralUnion && stringLiteralUnion . length > 0 ) {
152+ this . emitStringLiteralUnion ( type , node ) ;
153+ continue ;
154+ }
148155 if ( this . isEnumType ( type ) ) {
149156 this . visitEnumType ( type , node ) ;
150157 continue ;
@@ -314,6 +321,78 @@ export class TypeProcessor {
314321 return ( symbol . flags & ts . SymbolFlags . Enum ) !== 0 ;
315322 }
316323
324+ dedupeSwiftEnumCaseNames ( items ) {
325+ const seen = new Map ( ) ;
326+ return items . map ( item => {
327+ const count = seen . get ( item . name ) ?? 0 ;
328+ seen . set ( item . name , count + 1 ) ;
329+ if ( count === 0 ) return item ;
330+ return { ...item , name : `${ item . name } _${ count + 1 } ` } ;
331+ } ) ;
332+ }
333+
334+ /**
335+ * Extract string literal values if the type is a union containing only string literals.
336+ * Returns null when any member is not a string literal.
337+ * @param {ts.Type } type
338+ * @returns {string[] | null }
339+ * @private
340+ */
341+ getStringLiteralUnionLiterals ( type ) {
342+ if ( ( type . flags & ts . TypeFlags . Union ) === 0 ) return null ;
343+ const symbol = type . getSymbol ( ) ?? type . aliasSymbol ;
344+ // Skip enums so we don't double-generate real enum declarations.
345+ if ( symbol && ( symbol . flags & ts . SymbolFlags . Enum ) !== 0 ) {
346+ return null ;
347+ }
348+ /** @type {ts.UnionType } */
349+ // @ts -ignore
350+ const unionType = type ;
351+ /** @type {string[] } */
352+ const literals = [ ] ;
353+ const seen = new Set ( ) ;
354+ for ( const member of unionType . types ) {
355+ if ( ( member . flags & ts . TypeFlags . StringLiteral ) === 0 ) {
356+ return null ;
357+ }
358+ // @ts -ignore value exists for string literal types
359+ const value = String ( member . value ) ;
360+ if ( seen . has ( value ) ) continue ;
361+ seen . add ( value ) ;
362+ literals . push ( value ) ;
363+ }
364+ return literals ;
365+ }
366+
367+ /**
368+ * @param {ts.Type } type
369+ * @param {ts.Node } diagnosticNode
370+ * @private
371+ */
372+ emitStringLiteralUnion ( type , diagnosticNode ) {
373+ const typeName = this . deriveTypeName ( type ) ;
374+ if ( ! typeName ) return ;
375+ if ( this . emittedStringLiteralUnionNames . has ( typeName ) ) return ;
376+ this . emittedStringLiteralUnionNames . add ( typeName ) ;
377+
378+ const literals = this . getStringLiteralUnionLiterals ( type ) ;
379+ if ( ! literals || literals . length === 0 ) return ;
380+
381+ const swiftEnumName = this . renderTypeIdentifier ( typeName ) ;
382+ /** @type {{ name: string, raw: string }[] } */
383+ const members = literals . map ( raw => ( { name : makeValidSwiftIdentifier ( String ( raw ) , { emptyFallback : "_case" } ) , raw : String ( raw ) } ) ) ;
384+ const deduped = this . dedupeSwiftEnumCaseNames ( members ) ;
385+
386+ this . emitDocComment ( diagnosticNode , { indent : "" } ) ;
387+ this . swiftLines . push ( `enum ${ swiftEnumName } : String {` ) ;
388+ for ( const { name, raw } of deduped ) {
389+ this . swiftLines . push ( ` case ${ this . renderIdentifier ( name ) } = "${ raw . replaceAll ( "\"" , "\\\"" ) } "` ) ;
390+ }
391+ this . swiftLines . push ( "}" ) ;
392+ this . swiftLines . push ( `extension ${ swiftEnumName } : _BridgedSwiftEnumNoPayload, _BridgedSwiftRawValueEnum {}` ) ;
393+ this . swiftLines . push ( "" ) ;
394+ }
395+
317396 /**
318397 * @param {ts.EnumDeclaration } node
319398 * @private
@@ -860,6 +939,7 @@ export class TypeProcessor {
860939 * @returns {string }
861940 */
862941 const convert = ( type ) => {
942+ const originalType = type ;
863943 // Handle nullable/undefined unions (e.g. T | null, T | undefined)
864944 const isUnionType = ( type . flags & ts . TypeFlags . Union ) !== 0 ;
865945 if ( isUnionType ) {
@@ -882,6 +962,15 @@ export class TypeProcessor {
882962 }
883963 return `JSUndefinedOr<${ wrapped } >` ;
884964 }
965+
966+ const stringLiteralUnion = this . getStringLiteralUnionLiterals ( type ) ;
967+ if ( stringLiteralUnion && stringLiteralUnion . length > 0 ) {
968+ const typeName = this . deriveTypeName ( originalType ) ?? this . deriveTypeName ( type ) ;
969+ if ( typeName ) {
970+ this . seenTypes . set ( originalType , node ) ;
971+ return this . renderTypeIdentifier ( typeName ) ;
972+ }
973+ }
885974 }
886975
887976 /** @type {Record<string, string> } */
@@ -911,6 +1000,12 @@ export class TypeProcessor {
9111000 return this . renderTypeIdentifier ( typeName ) ;
9121001 }
9131002
1003+ const stringLiteralUnion = this . getStringLiteralUnionLiterals ( type ) ;
1004+ if ( stringLiteralUnion && stringLiteralUnion . length > 0 ) {
1005+ this . seenTypes . set ( type , node ) ;
1006+ return this . renderTypeIdentifier ( this . deriveTypeName ( type ) ?? this . checker . typeToString ( type ) ) ;
1007+ }
1008+
9141009 if ( this . checker . isTupleType ( type ) || type . getCallSignatures ( ) . length > 0 ) {
9151010 return "JSObject" ;
9161011 }
0 commit comments