@@ -474,6 +474,28 @@ export class CommonToolsFormatter implements TypeFormatter {
474474 typeNode : ts . TypeNode ,
475475 context : GenerationContext ,
476476 ) : unknown {
477+ // Handle typeof expressions (TypeQuery nodes)
478+ // These reference a variable's value, like: typeof defaultRoutes
479+ if ( ts . isTypeQueryNode ( typeNode ) ) {
480+ return this . extractValueFromTypeQuery ( typeNode , context ) ;
481+ }
482+
483+ // Handle type references that represent empty objects
484+ // This includes Record<string, never>, Record<K, never>, and similar mapped types
485+ if ( ts . isTypeReferenceNode ( typeNode ) && typeNode . typeArguments ) {
486+ // For mapped types like Record<K, V>, if V is never, the result is an empty object
487+ // Check the last type argument (the value type in mapped types)
488+ const lastTypeArg =
489+ typeNode . typeArguments [ typeNode . typeArguments . length - 1 ] ;
490+ if ( lastTypeArg ) {
491+ const lastType = context . typeChecker . getTypeFromTypeNode ( lastTypeArg ) ;
492+ // If the value type is never, this represents an empty object
493+ if ( lastType . flags & ts . TypeFlags . Never ) {
494+ return { } ;
495+ }
496+ }
497+ }
498+
477499 // Handle literal types
478500 if ( ts . isLiteralTypeNode ( typeNode ) ) {
479501 const literal = typeNode . literal ;
@@ -549,102 +571,131 @@ export class CommonToolsFormatter implements TypeFormatter {
549571 return undefined ;
550572 }
551573
552- private extractComplexDefaultFromTypeSymbol (
553- type : ts . Type ,
554- _symbol : ts . Symbol ,
574+ private extractValueFromTypeQuery (
575+ typeQueryNode : ts . TypeQueryNode ,
555576 context : GenerationContext ,
556577 ) : unknown {
557- // For now, try to extract from type string - this is a fallback approach
558- const typeString = context . typeChecker . typeToString ( type ) ;
559-
560- // Handle array literals like ["item1", "item2"]
561- if ( typeString . startsWith ( "[" ) && typeString . endsWith ( "]" ) ) {
562- try {
563- return JSON . parse ( typeString ) ;
564- } catch {
565- // If JSON parsing fails, try simpler extraction
566- return this . parseArrayLiteral ( typeString ) ;
567- }
578+ // Get the entity name being queried (e.g., "defaultRoutes" in "typeof defaultRoutes")
579+ const exprName = typeQueryNode . exprName ;
580+
581+ // Get the symbol for the referenced entity
582+ const symbol = context . typeChecker . getSymbolAtLocation ( exprName ) ;
583+ if ( ! symbol ) {
584+ return undefined ;
568585 }
569586
570- // Handle object literals like { theme: "dark", count: 10 }
571- if ( typeString . startsWith ( "{" ) && typeString . endsWith ( "}" ) ) {
572- try {
573- // Convert TS object syntax to JSON syntax
574- const jsonString = typeString
575- . replace ( / ( \w + ) : / g, '"$1":' ) // Quote property names
576- . replace ( / ' / g, '"' ) ; // Convert single quotes to double quotes
577- return JSON . parse ( jsonString ) ;
578- } catch {
579- // If JSON parsing fails, return a simpler fallback
580- return this . parseObjectLiteral ( typeString ) ;
581- }
587+ return this . extractValueFromSymbol ( symbol , context ) ;
588+ }
589+
590+ /**
591+ * Extract a runtime value from a symbol's value declaration.
592+ * Works for variables with initializers like: const foo = [1, 2, 3]
593+ */
594+ private extractValueFromSymbol (
595+ symbol : ts . Symbol ,
596+ context : GenerationContext ,
597+ ) : unknown {
598+ const valueDeclaration = symbol . valueDeclaration ;
599+ if ( ! valueDeclaration ) {
600+ return undefined ;
601+ }
602+
603+ // Check if it's a variable declaration with an initializer
604+ if (
605+ ts . isVariableDeclaration ( valueDeclaration ) &&
606+ valueDeclaration . initializer
607+ ) {
608+ return this . extractValueFromExpression (
609+ valueDeclaration . initializer ,
610+ context ,
611+ ) ;
582612 }
583613
584614 return undefined ;
585615 }
586616
587- private parseArrayLiteral ( str : string ) : unknown [ ] {
588- // Simple array parsing for basic cases
589- if ( str === "[]" ) return [ ] ;
590-
591- // Remove brackets and split by comma
592- const inner = str . slice ( 1 , - 1 ) ;
593- if ( ! inner . trim ( ) ) return [ ] ;
617+ private extractValueFromExpression (
618+ expr : ts . Expression ,
619+ context : GenerationContext ,
620+ ) : unknown {
621+ // Handle array literals like [1, 2, 3] or [{ id: "a" }, { id: "b" }]
622+ if ( ts . isArrayLiteralExpression ( expr ) ) {
623+ return expr . elements . map ( ( element ) =>
624+ this . extractValueFromExpression ( element , context )
625+ ) ;
626+ }
594627
595- const items = inner . split ( "," ) . map ( ( item ) => {
596- const trimmed = item . trim ( ) ;
597- if ( trimmed . startsWith ( '"' ) && trimmed . endsWith ( '"' ) ) {
598- return trimmed . slice ( 1 , - 1 ) ; // String literal
599- }
600- if ( trimmed . startsWith ( "'" ) && trimmed . endsWith ( "'" ) ) {
601- return trimmed . slice ( 1 , - 1 ) ; // String literal
602- }
603- if ( ! isNaN ( Number ( trimmed ) ) ) {
604- return Number ( trimmed ) ; // Number literal
628+ // Handle object literals like { id: "a", name: "test" }
629+ if ( ts . isObjectLiteralExpression ( expr ) ) {
630+ const obj : Record < string , unknown > = { } ;
631+ for ( const property of expr . properties ) {
632+ if (
633+ ts . isPropertyAssignment ( property ) && ts . isIdentifier ( property . name )
634+ ) {
635+ const propName = property . name . text ;
636+ obj [ propName ] = this . extractValueFromExpression (
637+ property . initializer ,
638+ context ,
639+ ) ;
640+ } else if ( ts . isShorthandPropertyAssignment ( property ) ) {
641+ // Handle shorthand like { id } where id is a variable
642+ const propName = property . name . text ;
643+ obj [ propName ] = this . extractValueFromExpression (
644+ property . name ,
645+ context ,
646+ ) ;
647+ }
605648 }
606- if ( trimmed === "true" ) return true ;
607- if ( trimmed === "false" ) return false ;
608- if ( trimmed === "null" ) return null ;
609- return trimmed ; // Fallback
610- } ) ;
649+ return obj ;
650+ }
651+
652+ // Handle string literals
653+ if ( ts . isStringLiteral ( expr ) ) {
654+ return expr . text ;
655+ }
656+
657+ // Handle numeric literals
658+ if ( ts . isNumericLiteral ( expr ) ) {
659+ return Number ( expr . text ) ;
660+ }
661+
662+ // Handle boolean literals
663+ if ( expr . kind === ts . SyntaxKind . TrueKeyword ) {
664+ return true ;
665+ }
666+ if ( expr . kind === ts . SyntaxKind . FalseKeyword ) {
667+ return false ;
668+ }
669+
670+ // Handle null
671+ if ( expr . kind === ts . SyntaxKind . NullKeyword ) {
672+ return null ;
673+ }
611674
612- return items ;
675+ // For more complex expressions, return undefined
676+ return undefined ;
613677 }
614678
615- private parseObjectLiteral ( str : string ) : Record < string , unknown > {
616- // Very basic object parsing - this is a fallback
617- const obj : Record < string , unknown > = { } ;
618-
619- // Remove braces
620- const inner = str . slice ( 1 , - 1 ) . trim ( ) ;
621- if ( ! inner ) return obj ;
622-
623- // This is a simplified parser - for more complex cases we'd need proper AST parsing
624- const pairs = inner . split ( "," ) ;
625- for ( const pair of pairs ) {
626- const [ key , ...valueParts ] = pair . split ( ":" ) ;
627- if ( key && valueParts . length > 0 ) {
628- const keyTrimmed = key . trim ( ) . replace ( / " / g, "" ) ;
629- const valueStr = valueParts . join ( ":" ) . trim ( ) ;
630-
631- // Parse simple values
632- if ( valueStr . startsWith ( '"' ) && valueStr . endsWith ( '"' ) ) {
633- obj [ keyTrimmed ] = valueStr . slice ( 1 , - 1 ) ;
634- } else if ( ! isNaN ( Number ( valueStr ) ) ) {
635- obj [ keyTrimmed ] = Number ( valueStr ) ;
636- } else if ( valueStr === "true" ) {
637- obj [ keyTrimmed ] = true ;
638- } else if ( valueStr === "false" ) {
639- obj [ keyTrimmed ] = false ;
640- } else if ( valueStr === "null" ) {
641- obj [ keyTrimmed ] = null ;
642- } else {
643- obj [ keyTrimmed ] = valueStr ;
644- }
645- }
679+ private extractComplexDefaultFromTypeSymbol (
680+ type : ts . Type ,
681+ symbol : ts . Symbol ,
682+ context : GenerationContext ,
683+ ) : unknown {
684+ // Try to extract from the symbol's value declaration initializer (AST-based)
685+ const extracted = this . extractValueFromSymbol ( symbol , context ) ;
686+ if ( extracted !== undefined ) {
687+ return extracted ;
688+ }
689+
690+ // Check if this is an empty object type (no properties, object type)
691+ // This handles cases like Record<string, never>
692+ if (
693+ ( type . flags & ts . TypeFlags . Object ) !== 0 &&
694+ context . typeChecker . getPropertiesOfType ( type ) . length === 0
695+ ) {
696+ return { } ;
646697 }
647698
648- return obj ;
699+ return undefined ;
649700 }
650701}
0 commit comments