@@ -54,6 +54,10 @@ 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 ( ) ;
59+ /** @type {Set<string> } */
60+ this . emittedStringLiteralUnionNames = new Set ( ) ;
5761
5862 /** @type {Set<string> } */
5963 this . visitedDeclarationKeys = new Set ( ) ;
@@ -145,6 +149,11 @@ export class TypeProcessor {
145149
146150 for ( const [ type , node ] of this . seenTypes ) {
147151 this . seenTypes . delete ( type ) ;
152+ const stringLiteralUnion = this . getStringLiteralUnionLiterals ( type ) ;
153+ if ( stringLiteralUnion && stringLiteralUnion . length > 0 ) {
154+ this . emitStringLiteralUnion ( type , node ) ;
155+ continue ;
156+ }
148157 if ( this . isEnumType ( type ) ) {
149158 this . visitEnumType ( type , node ) ;
150159 continue ;
@@ -296,6 +305,73 @@ export class TypeProcessor {
296305 return ( symbol . flags & ts . SymbolFlags . Enum ) !== 0 ;
297306 }
298307
308+ dedupeSwiftEnumCaseNames ( items ) {
309+ const seen = new Map ( ) ;
310+ return items . map ( item => {
311+ const count = seen . get ( item . name ) ?? 0 ;
312+ seen . set ( item . name , count + 1 ) ;
313+ if ( count === 0 ) return item ;
314+ return { ...item , name : `${ item . name } _${ count + 1 } ` } ;
315+ } ) ;
316+ }
317+
318+ /**
319+ * Extract string literal values if the type is a union containing only string literals.
320+ * Returns null when any member is not a string literal.
321+ * @param {ts.Type } type
322+ * @returns {string[] | null }
323+ * @private
324+ */
325+ getStringLiteralUnionLiterals ( type ) {
326+ if ( ( type . flags & ts . TypeFlags . Union ) === 0 ) return null ;
327+ /** @type {ts.UnionType } */
328+ // @ts -ignore
329+ const unionType = type ;
330+ /** @type {string[] } */
331+ const literals = [ ] ;
332+ const seen = new Set ( ) ;
333+ for ( const member of unionType . types ) {
334+ if ( ( member . flags & ts . TypeFlags . StringLiteral ) === 0 ) {
335+ return null ;
336+ }
337+ // @ts -ignore value exists for string literal types
338+ const value = String ( member . value ) ;
339+ if ( seen . has ( value ) ) continue ;
340+ seen . add ( value ) ;
341+ literals . push ( value ) ;
342+ }
343+ return literals ;
344+ }
345+
346+ /**
347+ * @param {ts.Type } type
348+ * @param {ts.Node } diagnosticNode
349+ * @private
350+ */
351+ emitStringLiteralUnion ( type , diagnosticNode ) {
352+ const typeName = this . deriveTypeName ( type ) ;
353+ if ( ! typeName ) return ;
354+ if ( this . emittedStringLiteralUnionNames . has ( typeName ) ) return ;
355+ this . emittedStringLiteralUnionNames . add ( typeName ) ;
356+
357+ const literals = this . getStringLiteralUnionLiterals ( type ) ;
358+ if ( ! literals || literals . length === 0 ) return ;
359+
360+ const swiftEnumName = this . renderTypeIdentifier ( typeName ) ;
361+ /** @type {{ name: string, raw: string }[] } */
362+ const members = literals . map ( raw => ( { name : makeValidSwiftIdentifier ( String ( raw ) , { emptyFallback : "_case" } ) , raw : String ( raw ) } ) ) ;
363+ const deduped = this . dedupeSwiftEnumCaseNames ( members ) ;
364+
365+ this . emitDocComment ( diagnosticNode , { indent : "" } ) ;
366+ this . swiftLines . push ( `enum ${ swiftEnumName } : String {` ) ;
367+ for ( const { name, raw } of deduped ) {
368+ this . swiftLines . push ( ` case ${ this . renderIdentifier ( name ) } = "${ raw . replaceAll ( "\"" , "\\\"" ) } "` ) ;
369+ }
370+ this . swiftLines . push ( "}" ) ;
371+ this . swiftLines . push ( `extension ${ swiftEnumName } : _BridgedSwiftEnumNoPayload, _BridgedSwiftRawValueEnum {}` ) ;
372+ this . swiftLines . push ( "" ) ;
373+ }
374+
299375 /**
300376 * @param {ts.EnumDeclaration } node
301377 * @private
@@ -841,6 +917,7 @@ export class TypeProcessor {
841917 * @returns {string }
842918 */
843919 const convert = ( type ) => {
920+ const originalType = type ;
844921 // Handle nullable/undefined unions (e.g. T | null, T | undefined)
845922 const isUnionType = ( type . flags & ts . TypeFlags . Union ) !== 0 ;
846923 if ( isUnionType ) {
@@ -863,6 +940,15 @@ export class TypeProcessor {
863940 }
864941 return `JSUndefinedOr<${ wrapped } >` ;
865942 }
943+
944+ const stringLiteralUnion = this . getStringLiteralUnionLiterals ( type ) ;
945+ if ( stringLiteralUnion && stringLiteralUnion . length > 0 ) {
946+ const typeName = this . deriveTypeName ( originalType ) ?? this . deriveTypeName ( type ) ;
947+ if ( typeName ) {
948+ this . seenTypes . set ( originalType , node ) ;
949+ return this . renderTypeIdentifier ( typeName ) ;
950+ }
951+ }
866952 }
867953
868954 /** @type {Record<string, string> } */
@@ -892,6 +978,12 @@ export class TypeProcessor {
892978 return this . renderTypeIdentifier ( typeName ) ;
893979 }
894980
981+ const stringLiteralUnion = this . getStringLiteralUnionLiterals ( type ) ;
982+ if ( stringLiteralUnion && stringLiteralUnion . length > 0 ) {
983+ this . seenTypes . set ( type , node ) ;
984+ return this . renderTypeIdentifier ( this . deriveTypeName ( type ) ?? this . checker . typeToString ( type ) ) ;
985+ }
986+
895987 if ( this . checker . isTupleType ( type ) || type . getCallSignatures ( ) . length > 0 ) {
896988 return "JSObject" ;
897989 }
0 commit comments