Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions docs/compiler-configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Compiler Configuration

The codegen system automatically adapts to TypeScript compiler options to ensure generated code is compatible with the target JavaScript environment.

## Script Target Detection

The system automatically determines the appropriate TypeScript script target using a two-tier approach:

1. **Project Configuration** - Uses the target specified in the ts-morph Project's compiler options
2. **Environment Detection** - When no explicit target is found, detects the appropriate target based on the runtime TypeScript version

## Identifier Validation

Property names in generated TypeBox objects are validated using TypeScript's built-in utilities:

- `ts.isIdentifierStart()` - validates first character
- `ts.isIdentifierPart()` - validates remaining characters

The validation respects the detected script target to ensure compatibility:

```typescript
// With ES5 target
interface Example {
validName: string // → validName: Type.String()
'invalid-name': number // → 'invalid-name': Type.Number()
'123invalid': boolean // → '123invalid': Type.Boolean()
}
```

## Configuration Management

The `CompilerConfig` singleton manages script target configuration:

- **Singleton Pattern** - Ensures consistent configuration across the application
- **Environment Detection** - Automatically detects appropriate targets from TypeScript version
- **Project Override** - Respects explicit targets from ts-morph Project configuration
- **Runtime Configuration** - Allows manual target specification when needed

## Environment-Based Target Detection

When no explicit target is specified in the project configuration, the system automatically detects an appropriate target based on the TypeScript version:

- **TypeScript 5.2+** → ES2023
- **TypeScript 5.0+** → ES2022
- **TypeScript 4.9+** → ES2022
- **TypeScript 4.7+** → ES2021
- **TypeScript 4.5+** → ES2020
- **TypeScript 4.2+** → ES2019
- **TypeScript 4.1+** → ES2018
- **TypeScript 4.0+** → ES2017
- **TypeScript 3.8+** → ES2017
- **TypeScript 3.6+** → ES2016
- **TypeScript 3.4+** → ES2015
- **Older versions** → ES5

This ensures generated code uses language features that are supported by the available TypeScript compiler, avoiding compatibility issues.

## Integration Points

The configuration system integrates with:

- **Input Handler** - Initializes config when creating source files
- **Code Generation** - Uses config for output file creation
- **Identifier Utils** - Validates property names with correct target
- **Object Handlers** - Determines property name formatting
8 changes: 8 additions & 0 deletions docs/handler-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ export abstract class BaseTypeHandler {
- `ObjectTypeHandler` - { prop: T }
- `InterfaceTypeHandler` - interface references

Object property names are extracted using the TypeScript compiler API through `PropertySignature.getNameNode()`. The system handles different property name formats:

- **Identifiers** (`prop`) - extracted using `nameNode.getText()` and preserved as identifiers
- **String literals** (`'prop-name'`, `"prop name"`) - extracted using `nameNode.getLiteralValue()` and validated for identifier compatibility
- **Numeric literals** (`123`) - extracted using `nameNode.getLiteralValue().toString()` and treated as identifiers

The system uses TypeScript's built-in character validation utilities (`ts.isIdentifierStart` and `ts.isIdentifierPart`) with runtime-determined script targets to determine if property names can be safely used as unquoted identifiers in the generated code. The script target is automatically determined from the ts-morph Project's compiler options, ensuring compatibility with the target JavaScript environment while maintaining optimal output format.

Comment on lines +33 to +40
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Correctness and clarity: numeric key handling and list punctuation

  • Numeric keys aren’t “treated as identifiers”; they’re emitted as numeric literals.
  • Minor grammar/punctuation fixes for list items.

Apply this diff:

-Object property names are extracted using the TypeScript compiler API through `PropertySignature.getNameNode()`. The system handles different property name formats:
+Object property names are extracted using the TypeScript compiler API via `PropertySignature.getNameNode()`. The system handles different property name formats:
 
-- **Identifiers** (`prop`) - extracted using `nameNode.getText()` and preserved as identifiers
-- **String literals** (`'prop-name'`, `"prop name"`) - extracted using `nameNode.getLiteralValue()` and validated for identifier compatibility
-- **Numeric literals** (`123`) - extracted using `nameNode.getLiteralValue().toString()` and treated as identifiers
+- **Identifiers** (`prop`): extracted using `nameNode.getText()` and preserved as identifiers.
+- **String literals** (`'prop-name'`, `"prop name"`): extracted using `nameNode.getLiteralValue()` and validated for identifier compatibility; emitted unquoted when safe, otherwise quoted.
+- **Numeric literals** (`123`): extracted using `nameNode.getLiteralValue().toString()` and emitted as numeric literals.
 
-The system uses TypeScript's built-in character validation utilities (`ts.isIdentifierStart` and `ts.isIdentifierPart`) with runtime-determined script targets to determine if property names can be safely used as unquoted identifiers in the generated code. The script target is automatically determined from the ts-morph Project's compiler options, ensuring compatibility with the target JavaScript environment while maintaining optimal output format.
+The system uses TypeScript's built-in identifier validation (`ts.isIdentifierStart`, `ts.isIdentifierPart`) with a runtime-determined script target to decide if property names can be safely emitted as unquoted identifiers. The script target is automatically derived from the ts-morph Project's compiler options, ensuring compatibility with the target JavaScript environment while maintaining optimal output format.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Object property names are extracted using the TypeScript compiler API through `PropertySignature.getNameNode()`. The system handles different property name formats:
- **Identifiers** (`prop`) - extracted using `nameNode.getText()` and preserved as identifiers
- **String literals** (`'prop-name'`, `"prop name"`) - extracted using `nameNode.getLiteralValue()` and validated for identifier compatibility
- **Numeric literals** (`123`) - extracted using `nameNode.getLiteralValue().toString()` and treated as identifiers
The system uses TypeScript's built-in character validation utilities (`ts.isIdentifierStart` and `ts.isIdentifierPart`) with runtime-determined script targets to determine if property names can be safely used as unquoted identifiers in the generated code. The script target is automatically determined from the ts-morph Project's compiler options, ensuring compatibility with the target JavaScript environment while maintaining optimal output format.
Object property names are extracted using the TypeScript compiler API via `PropertySignature.getNameNode()`. The system handles different property name formats:
- **Identifiers** (`prop`): extracted using `nameNode.getText()` and preserved as identifiers.
- **String literals** (`'prop-name'`, `"prop name"`): extracted using `nameNode.getLiteralValue()` and validated for identifier compatibility; emitted unquoted when safe, otherwise quoted.
- **Numeric literals** (`123`): extracted using `nameNode.getLiteralValue().toString()` and emitted as numeric literals.
The system uses TypeScript's built-in identifier validation (`ts.isIdentifierStart`, `ts.isIdentifierPart`) with a runtime-determined script target to decide if property names can be safely emitted as unquoted identifiers. The script target is automatically derived from the ts-morph Project's compiler options, ensuring compatibility with the target JavaScript environment while maintaining optimal output format.
🧰 Tools
🪛 LanguageTool

[grammar] ~35-~35: There might be a mistake here.
Context: ....getText() and preserved as identifiers - **String literals** ('prop-name', "prop...

(QB_NEW_EN)


[grammar] ~36-~36: There might be a mistake here.
Context: ...d validated for identifier compatibility - Numeric literals (123) - extracted u...

(QB_NEW_EN)

🤖 Prompt for AI Agents
In docs/handler-system.md around lines 33 to 40, update the wording to correctly
state that numeric property keys are emitted as numeric literals (not “treated
as identifiers”) and adjust the list item punctuation/grammar: change the
“Numeric literals (`123`) - extracted using
`nameNode.getLiteralValue().toString()` and treated as identifiers” line to
indicate they are emitted as numeric literals (e.g., extracted via
`getLiteralValue()` and emitted as numbers), and clean up punctuation on all
three list items so each item is a parallel, grammatically consistent sentence
with proper commas/periods.

### Utility Types

- `PartialTypeHandler` - Partial<T>
Expand Down
1 change: 1 addition & 0 deletions docs/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,6 @@ export type User = Static<typeof User>
- [architecture.md](./architecture.md) - System architecture
- [parser-system.md](./parser-system.md) - TypeScript parsing
- [handler-system.md](./handler-system.md) - Type conversion
- [compiler-configuration.md](./compiler-configuration.md) - Compiler options and script targets
- [dependency-management.md](./dependency-management.md) - Dependency analysis
- [testing.md](./testing.md) - Testing
12 changes: 12 additions & 0 deletions docs/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@ NodeTypeUtils.isTypeReference(node, 'Partial') // Check if node is Partial<T>
NodeTypeUtils.isReadonlyArrayType(node) // Check if readonly T[]
```

### Identifier Validation

`src/utils/identifier-utils.ts` - JavaScript identifier validation:

```typescript
isValidIdentifier('validName') // true
isValidIdentifier('123invalid') // false
isValidIdentifier('𝒜') // true - supports Unicode characters
```

Validates JavaScript identifiers using TypeScript's built-in utilities with full Unicode support, including characters outside the Basic Multilingual Plane.

### Template Literal Processing

`src/utils/template-literal-type-processor.ts` - Processes template literal types:
Expand Down
6 changes: 5 additions & 1 deletion src/handlers/typebox/collection/array-type-handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { CollectionBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/collection/collection-base-handler'
import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call'
import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
import { ArrayTypeNode, Node, ts } from 'ts-morph'

export class ArrayTypeHandler extends CollectionBaseHandler {
Expand All @@ -7,6 +9,8 @@ export class ArrayTypeHandler extends CollectionBaseHandler {
}

handle(node: ArrayTypeNode): ts.Expression {
return this.processSingleType(node.getElementTypeNode(), 'Array')
const typeboxType = getTypeBoxType(node.getElementTypeNode())

return makeTypeCall('Array', [typeboxType])
}
}
10 changes: 0 additions & 10 deletions src/handlers/typebox/collection/collection-base-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,4 @@ export abstract class CollectionBaseHandler extends BaseTypeHandler {

return makeTypeCall(typeBoxFunction, [arrayLiteral])
}

protected processSingleType(node: Node, typeBoxFunction: string): ts.Expression {
return makeTypeCall(typeBoxFunction, [getTypeBoxType(node)])
}

protected validateNonEmptyCollection(nodes: Node[], typeName: string): void {
if (nodes.length === 0) {
throw new Error(`${typeName} must have at least one type`)
}
}
}
3 changes: 1 addition & 2 deletions src/handlers/typebox/date-type-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ export class DateTypeHandler extends BaseTypeHandler {
return Node.isIdentifier(typeName) && typeName.getText() === 'Date'
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
handle(_node: TypeReferenceNode): ts.Expression {
handle(): ts.Expression {
return makeTypeCall('Date')
}
Comment on lines +12 to 14
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Confirm base signature: handler.handle now parameterless

Assuming BaseTypeHandler’s contract changed to handle(): ts.Expression. If not, this override will be incompatible.

Run:


🏁 Script executed:

#!/bin/bash
rg -nP "class\s+BaseTypeHandler\b.*?\bhandle\s*\(" -U -C2

Length of output: 59


🏁 Script executed:

#!/bin/bash
# Locate the BaseTypeHandler class definition
rg -nP "class\s+BaseTypeHandler\b" -n -C3
# Inspect BaseTypeHandler for handle signature
rg -nP "BaseTypeHandler[^}]*handle\s*\(" -n -C3

Length of output: 865


Ensure handle override matches BaseTypeHandler signature
In src/handlers/typebox/date-type-handler.ts, change

handle(): ts.Expression {
  return makeTypeCall('Date')
}

to

handle(node: Node): ts.Expression {
  return makeTypeCall('Date')
}

to satisfy

abstract handle(node: Node): ts.Expression

in BaseTypeHandler.

🤖 Prompt for AI Agents
In src/handlers/typebox/date-type-handler.ts around lines 12 to 14, the handle
method currently has no parameters but must match the BaseTypeHandler signature;
change the method signature to accept a single parameter node: Node (i.e.
handle(node: Node): ts.Expression) while keeping the body returning
makeTypeCall('Date') so the override matches abstract handle(node: Node):
ts.Expression.

}
21 changes: 9 additions & 12 deletions src/handlers/typebox/indexed-access-type-handler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler'
import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call'
import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
import { IndexedAccessTypeNode, Node, ts } from 'ts-morph'
import { IndexedAccessTypeNode, Node, ts, TypeNode } from 'ts-morph'

export class IndexedAccessTypeHandler extends BaseTypeHandler {
canHandle(node: Node): boolean {
Expand All @@ -14,8 +14,8 @@ export class IndexedAccessTypeHandler extends BaseTypeHandler {

// Handle special case: typeof A[number] where A is a readonly tuple
if (
objectType?.isKind(ts.SyntaxKind.TypeQuery) &&
indexType?.isKind(ts.SyntaxKind.NumberKeyword)
objectType.isKind(ts.SyntaxKind.TypeQuery) &&
indexType.isKind(ts.SyntaxKind.NumberKeyword)
) {
return this.handleTypeofArrayAccess(objectType, node)
}
Expand All @@ -42,31 +42,28 @@ export class IndexedAccessTypeHandler extends BaseTypeHandler {
const typeAlias = sourceFile.getTypeAlias(typeName)
if (typeAlias) {
const tupleUnion = this.extractTupleUnion(typeAlias.getTypeNode())
if (tupleUnion) {
return tupleUnion
}
if (tupleUnion) return tupleUnion
}

// Then try to find a variable declaration
const variableDeclaration = sourceFile.getVariableDeclaration(typeName)
if (variableDeclaration) {
const tupleUnion = this.extractTupleUnion(variableDeclaration.getTypeNode())
if (tupleUnion) {
return tupleUnion
}
if (tupleUnion) return tupleUnion
}
}

// Fallback to default Index behavior
const typeboxObjectType = getTypeBoxType(typeQuery)
const typeboxIndexType = getTypeBoxType(indexedAccessType.getIndexTypeNode())

return makeTypeCall('Index', [typeboxObjectType, typeboxIndexType])
}

private extractTupleUnion(typeNode: Node | undefined): ts.Expression | null {
private extractTupleUnion(typeNode: TypeNode | undefined): ts.Expression | null {
if (!typeNode) return null

let actualTupleType: Node | undefined = typeNode
let actualTupleType: TypeNode = typeNode

// Handle readonly modifier (TypeOperator)
if (typeNode.isKind(ts.SyntaxKind.TypeOperator)) {
Expand All @@ -75,7 +72,7 @@ export class IndexedAccessTypeHandler extends BaseTypeHandler {
}

// Check if it's a tuple type
if (actualTupleType?.isKind(ts.SyntaxKind.TupleType)) {
if (actualTupleType.isKind(ts.SyntaxKind.TupleType)) {
const tupleType = actualTupleType.asKindOrThrow(ts.SyntaxKind.TupleType)
const elements = tupleType.getElements()

Expand Down
12 changes: 0 additions & 12 deletions src/handlers/typebox/keyof-type-handler.ts

This file was deleted.

11 changes: 3 additions & 8 deletions src/handlers/typebox/literal-type-handler.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler'
import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
import { Node, SyntaxKind, ts } from 'ts-morph'
import { LiteralTypeNode, Node, SyntaxKind, ts } from 'ts-morph'

export class LiteralTypeHandler extends BaseTypeHandler {
canHandle(node: Node): boolean {
return Node.isLiteralTypeNode(node) || Node.isTrueLiteral(node) || Node.isFalseLiteral(node)
}
Comment on lines 6 to 8
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix canHandle/handle mismatch (can cause runtime errors).

canHandle accepts True/False nodes, but handle expects a LiteralTypeNode and immediately calls getLiteral(). If a bare True/False node ever routes here, this will blow up.

Apply this diff to restrict canHandle to literal type nodes only:

-  canHandle(node: Node): boolean {
-    return Node.isLiteralTypeNode(node) || Node.isTrueLiteral(node) || Node.isFalseLiteral(node)
-  }
+  canHandle(node: Node): boolean {
+    return Node.isLiteralTypeNode(node)
+  }
🤖 Prompt for AI Agents
In src/handlers/typebox/literal-type-handler.ts around lines 6 to 8, canHandle
currently returns true for bare True/False literal nodes as well as
LiteralTypeNodes, but handle assumes a LiteralTypeNode and calls getLiteral(),
which will fail for bare True/False nodes; change canHandle to only return
Node.isLiteralTypeNode(node) (remove the checks for Node.isTrueLiteral and
Node.isFalseLiteral) so only LiteralTypeNodes are routed here and handle's
getLiteral() call is safe.


handle(node: Node): ts.Expression {
if (!Node.isLiteralTypeNode(node)) {
return makeTypeCall('Any')
}

handle(node: LiteralTypeNode): ts.Expression {
const literal = node.getLiteral()
const literalKind = literal.getKind()

switch (literalKind) {
switch (literal.getKind()) {
case SyntaxKind.StringLiteral:
return makeTypeCall('Literal', [
ts.factory.createStringLiteral(literal.getText().slice(1, -1)),
Expand Down
37 changes: 5 additions & 32 deletions src/handlers/typebox/object/interface-type-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,44 +8,19 @@ export class InterfaceTypeHandler extends ObjectLikeBaseHandler {
}

handle(node: InterfaceDeclaration): ts.Expression {
const typeParameters = node.getTypeParameters()
const heritageClauses = node.getHeritageClauses()
const baseObjectType = this.createObjectType(this.processProperties(node.getProperties()))

// For generic interfaces, return raw TypeBox expression
// The parser will handle wrapping it in an arrow function using GenericTypeUtils
if (typeParameters.length > 0) {
// For generic interfaces, handle inheritance here and return raw expression
if (heritageClauses.length === 0) {
return baseObjectType
}

const extendedTypes = this.collectExtendedTypes(heritageClauses)

if (extendedTypes.length === 0) {
return baseObjectType
}

// Create composite with extended types first, then the current interface
const allTypes = [...extendedTypes, baseObjectType]
return makeTypeCall('Composite', [ts.factory.createArrayLiteralExpression(allTypes, true)])
}

// For non-generic interfaces, handle as before
if (heritageClauses.length === 0) {
return baseObjectType
}
if (heritageClauses.length === 0) return baseObjectType

const extendedTypes = this.collectExtendedTypes(heritageClauses)

if (extendedTypes.length === 0) {
return baseObjectType
}
if (extendedTypes.length === 0) return baseObjectType

// Create composite with extended types first, then the current interface
const allTypes = [...extendedTypes, baseObjectType]
const expression = ts.factory.createArrayLiteralExpression(allTypes, true)

return makeTypeCall('Composite', [ts.factory.createArrayLiteralExpression(allTypes, true)])
return makeTypeCall('Composite', [expression])
}
Comment on lines +21 to 24
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Bug: creating identifiers from generic/qualified type text yields invalid AST; use node-driven conversion instead.

ts.factory.createIdentifier(typeText) and regex-based parsing will break for:

  • Qualified names (NS.Foo) — identifier cannot contain dots.
  • Generic types (Foo<Bar,Baz>) — not a valid identifier.
    Replace with getTypeBoxType(typeNode) to build correct expressions; drop the brittle regex helpers.
-    const expression = ts.factory.createArrayLiteralExpression(allTypes, true)
+    const expression = ts.factory.createArrayLiteralExpression(allTypes, true)
-        const typeText = typeNode.getText()
-        extendedTypes.push(
-          this.parseGenericTypeCall(typeText) ?? ts.factory.createIdentifier(typeText),
-        )
+        extendedTypes.push(getTypeBoxType(typeNode))

Add import (outside the changed hunk):

import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call'

Then remove parseGenericTypeCall and createTypeExpression helpers; they’re no longer needed and are incomplete (single type arg only).

Also applies to: 60-66

🤖 Prompt for AI Agents
In src/handlers/typebox/object/interface-type-handler.ts around lines 21-24 (and
also apply same change to lines ~60-66), the code builds TypeBox AST by creating
identifiers from type text and using regex parsing which fails for qualified
names (NS.Foo) and generics (Foo<Bar>)—replace those brittle helpers by
importing getTypeBoxType from
'@daxserver/validation-schema-codegen/utils/typebox-call' and use
getTypeBoxType(typeNode) to produce the proper expression nodes instead of
ts.factory.createIdentifier(typeText) or
parseGenericTypeCall/createTypeExpression; remove the parseGenericTypeCall and
createTypeExpression helpers entirely and update call sites to pass the original
type AST node into getTypeBoxType so qualified and generic types are correctly
converted.


private parseGenericTypeCall(typeText: string): ts.Expression | null {
Expand Down Expand Up @@ -82,9 +57,7 @@ export class InterfaceTypeHandler extends ObjectLikeBaseHandler {
const extendedTypes: ts.Expression[] = []

for (const heritageClause of heritageClauses) {
if (heritageClause.getToken() !== ts.SyntaxKind.ExtendsKeyword) {
continue
}
if (heritageClause.getToken() !== ts.SyntaxKind.ExtendsKeyword) continue

for (const typeNode of heritageClause.getTypeNodes()) {
const typeText = typeNode.getText()
Expand Down
44 changes: 33 additions & 11 deletions src/handlers/typebox/object/object-like-base-handler.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler'
import { isValidIdentifier } from '@daxserver/validation-schema-codegen/utils/identifier-utils'
import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call'
import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
import { PropertySignature, ts } from 'ts-morph'
import { Node, PropertySignature, ts } from 'ts-morph'

export abstract class ObjectLikeBaseHandler extends BaseTypeHandler {
protected processProperties(properties: PropertySignature[]): ts.PropertyAssignment[] {
const propertyAssignments: ts.PropertyAssignment[] = []

for (const prop of properties) {
const propName = prop.getName()
const propTypeNode = prop.getTypeNode()
if (!propTypeNode) continue

if (!propTypeNode) {
continue
}

const outputNameNode = this.extractPropertyNameInfo(prop)
const valueExpr = getTypeBoxType(propTypeNode)
const isAlreadyOptional =
ts.isCallExpression(valueExpr) &&
Expand All @@ -26,11 +24,7 @@ export abstract class ObjectLikeBaseHandler extends BaseTypeHandler {
? makeTypeCall('Optional', [valueExpr])
: valueExpr

const nameNode = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(propName)
? ts.factory.createIdentifier(propName)
: ts.factory.createStringLiteral(propName)

propertyAssignments.push(ts.factory.createPropertyAssignment(nameNode, maybeOptional))
propertyAssignments.push(ts.factory.createPropertyAssignment(outputNameNode, maybeOptional))
}

return propertyAssignments
Expand All @@ -41,4 +35,32 @@ export abstract class ObjectLikeBaseHandler extends BaseTypeHandler {

return makeTypeCall('Object', [objectLiteral])
}

private extractPropertyNameInfo(prop: PropertySignature): ts.PropertyName {
const nameNode = prop.getNameNode()
let propName: string
let shouldUseIdentifier: boolean

if (Node.isIdentifier(nameNode)) {
// If it was originally an identifier, keep it as an identifier
propName = nameNode.getText()
shouldUseIdentifier = true
} else if (Node.isStringLiteral(nameNode)) {
// For quoted properties, get the literal value and check if it can be an identifier
propName = nameNode.getLiteralValue()
shouldUseIdentifier = isValidIdentifier(propName)
} else if (Node.isNumericLiteral(nameNode)) {
// Numeric properties can be used as identifiers
propName = nameNode.getLiteralValue().toString()
shouldUseIdentifier = true
} else {
Comment on lines +53 to +56
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Bug: numeric keys emitted as Identifier nodes

Creating an Identifier with text "123" is invalid; numeric keys must be a NumericLiteral node. This can lead to malformed AST and potential printer/syntax issues.

Apply this diff:

-    } else if (Node.isNumericLiteral(nameNode)) {
-      // Numeric properties can be used as identifiers
-      propName = nameNode.getLiteralValue().toString()
-      shouldUseIdentifier = true
+    } else if (Node.isNumericLiteral(nameNode)) {
+      // Preserve numeric keys as numeric literals
+      return ts.factory.createNumericLiteral(nameNode.getLiteralValue().toString())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Numeric properties can be used as identifiers
propName = nameNode.getLiteralValue().toString()
shouldUseIdentifier = true
} else {
} else if (Node.isNumericLiteral(nameNode)) {
// Preserve numeric keys as numeric literals
return ts.factory.createNumericLiteral(nameNode.getLiteralValue().toString())
} else {
🤖 Prompt for AI Agents
In src/handlers/typebox/object/object-like-base-handler.ts around lines 54 to
57, numeric object keys are currently treated as Identifier nodes which is
invalid (e.g., "123" must be a NumericLiteral). Change the logic so when
nameNode.getLiteralValue() is numeric (or represents a numeric string), do not
mark shouldUseIdentifier true; instead keep shouldUseIdentifier false and ensure
the emitter creates a NumericLiteral node (using the literal value as a number)
for the property name; preserve the string propName only for non-numeric
identifiers.

// Fallback for any other cases
propName = prop.getName()
shouldUseIdentifier = isValidIdentifier(propName)
}

return shouldUseIdentifier
? ts.factory.createIdentifier(propName)
: ts.factory.createStringLiteral(propName)
}
}
12 changes: 0 additions & 12 deletions src/handlers/typebox/readonly-array-type-handler.ts

This file was deleted.

Loading