diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 9fa1c98..a1df5d2 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -24,9 +24,9 @@
### Supported TypeScript Constructs
- **Type Definitions**: Type aliases, interfaces, enums, and function declarations
-- **Generic Types**: Generic interfaces and type parameters with proper constraint handling
+- **Generic Types**: Generic interfaces and type aliases with type parameters and proper constraint handling
- **Complex Types**: Union and intersection types, nested object structures, template literal types
-- **Utility Types**: Built-in support for Pick, Omit, Partial, Required, Record, and other TypeScript utility types
+- **Utility Types**: Built-in support for Pick, Omit, Partial, Required, Record, Readonly, and other TypeScript utility types
- **Advanced Features**: Conditional types, mapped types, keyof operators, indexed access types
- **Import Resolution**: Cross-file type dependencies with qualified naming and circular dependency handling
@@ -39,7 +39,7 @@ The main logic for code generation resides in the orchestrates the entire code generation process:
1. **Input Processing**: Creates a `SourceFile` from input using `createSourceFileFromInput`
-2. **Generic Interface Detection**: Checks for generic interfaces to determine required TypeBox imports
+2. **Generic Type Detection**: Checks for generic interfaces and type aliases to determine required TypeBox imports (including `TSchema`)
3. **Output File Creation**: Creates a new output file with necessary `@sinclair/typebox` imports using `createOutputFile`
4. **Dependency Traversal**: Uses `DependencyTraversal` to analyze and sort all type dependencies
5. **Code Generation**: Processes sorted nodes using `TypeBoxPrinter` in `printSortedNodes`
@@ -113,8 +113,8 @@ The pr
#### Specialized Parsers
-1. **InterfaceParser**: - Handles both regular and generic interfaces
-2. **TypeAliasParser**: - Processes type alias declarations
+1. **InterfaceParser**: - Handles both regular and generic interfaces using the unified `GenericTypeUtils` flow for consistency with type aliases
+2. **TypeAliasParser**: - Processes both regular and generic type alias declarations using `GenericTypeUtils.createGenericArrowFunction`
3. **EnumParser**: - Handles enum declarations
4. **FunctionParser**: - Processes function declarations
5. **ImportParser**: - Handles import resolution
@@ -127,20 +127,42 @@ The handler system in and specialized base classes
2. **Collection Handlers**: , , ,
-3. **Object Handlers**: ,
+3. **Object Handlers**: (returns raw TypeBox expressions for generic interfaces, allowing parser-level arrow function wrapping),
4. **Reference Handlers**: , , , ,
-5. **Simple Handlers**: ,
-6. **Advanced Handlers**: , , ,
-7. **Function Handlers**:
-8. **Type Query Handlers**: ,
-9. **Access Handlers**: ,
+5. **Readonly Handlers**: ,
+6. **Simple Handlers**: ,
+7. **Advanced Handlers**: , ,
+8. **Function Handlers**:
+9. **Type Query Handlers**: ,
+10. **Access Handlers**: ,
+
+#### Readonly Type Handling
+
+The system provides comprehensive support for TypeScript's two distinct readonly constructs through a dual-handler approach:
+
+1. **Readonly Utility Type**: `Readonly` - Handled by
+ - Registered as a type reference handler for `Readonly` type references
+ - Processes `TypeReferenceNode` with identifier "Readonly"
+ - Generates `Type.Readonly(innerType)` for utility type syntax
+
+2. **Readonly Array Modifier**: `readonly T[]` - Handled by
+ - Extends `TypeOperatorBaseHandler` for `ReadonlyKeyword` operator
+ - Processes `TypeOperatorTypeNode` with `SyntaxKind.ReadonlyKeyword`
+ - Generates `Type.Readonly(innerType)` for array modifier syntax
+ - Registered as a fallback handler to handle complex readonly patterns
+
+This dual approach ensures proper handling of both TypeScript readonly constructs:
+
+- `type ReadonlyUser = Readonly` (utility type)
+- `type ReadonlyArray = readonly string[]` (array modifier)
+- `type ReadonlyTuple = readonly [string, number]` (tuple modifier)
#### Handler Management
The class orchestrates all handlers through:
- **Handler Caching**: Caches handler instances for performance optimization
-- **Fallback System**: Provides fallback handlers for complex cases
+- **Fallback System**: Provides fallback handlers for complex cases including readonly array modifiers
### Import Resolution
@@ -181,6 +203,39 @@ export interface InputOptions {
- **Source Code Input**: Processes TypeScript code directly from strings with validation
- **Project Context**: Enables proper relative import resolution when working with in-memory source files
+### Generic Type Support
+
+The codebase provides comprehensive support for both generic interfaces and generic type aliases, enabling complex type transformations and reusable type definitions.
+
+#### Generic Type Aliases
+
+The `TypeAliasParser` handles both regular and generic type aliases through specialized processing:
+
+1. **Type Parameter Detection**: Automatically detects type parameters using `typeAlias.getTypeParameters()`
+2. **Function Generation**: Creates TypeBox functions for generic type aliases with proper parameter constraints
+3. **TSchema Constraints**: Applies `TSchema` constraints to all type parameters for TypeBox compatibility
+4. **Static Type Generation**: Generates corresponding TypeScript type aliases using `Static>>`
+
+#### Generic Interface Support
+
+Generic interfaces are processed through the `InterfaceParser` using a consistent architectural pattern that mirrors the type alias flow:
+
+1. **Unified Generic Processing**: The interface parser now uses the same `GenericTypeUtils.createGenericArrowFunction` flow as type aliases for consistency
+2. **Raw Expression Handling**: The `InterfaceTypeHandler` returns raw TypeBox expressions for generic interfaces, allowing the parser to handle arrow function wrapping
+3. **Parameter Constraint Handling**: Converts TypeScript type parameter constraints to `TSchema` constraints using shared utilities
+4. **Function-Based Schema Generation**: Creates TypeBox schema functions that accept type parameters through the standardized generic arrow function pattern
+5. **Type Safety Preservation**: Maintains full TypeScript type safety through proper static type aliases using `Static>>`
+6. **Architectural Consistency**: Both generic interfaces and type aliases now follow the same code generation pattern, improving maintainability and reducing duplication
+
+#### Complex Generic Scenarios
+
+The system supports advanced generic patterns including:
+
+- **Multiple Type Parameters**: Functions with multiple generic parameters (e.g., `ApiResponse`)
+- **Nested Generic Types**: Generic types that reference other generic types
+- **Utility Type Combinations**: Complex combinations like `Partial>>`
+- **Type Parameter Propagation**: Proper handling of type parameters across nested type references
+
### Interface Inheritance
The codebase provides comprehensive support for TypeScript interface inheritance through a sophisticated dependency resolution and code generation system:
@@ -234,6 +289,16 @@ The directory provides essential
- **TypeBox Expression Generation**: Converts extracted keys into appropriate TypeBox array expressions
- **Shared Utilities**: Provides reusable key extraction logic for Pick, Omit, and other utility type handlers to avoid code duplication
+#### Generic Type Utilities
+
+The module provides shared utilities for consistent generic type handling across parsers:
+
+- **Generic Arrow Function Creation**: `createGenericArrowFunction` creates standardized arrow functions for generic types with proper type parameter constraints
+- **Type Parameter Processing**: Converts TypeScript type parameters to TypeBox-compatible function parameters with `TSchema` constraints
+- **Variable Statement Generation**: `addTypeBoxVariableStatement` creates consistent variable declarations for TypeBox schemas
+- **Generic Type Alias Generation**: `addGenericTypeAlias` creates standardized static type aliases using `Static>>`
+- **Architectural Consistency**: Ensures both interface and type alias parsers follow the same generic type processing pattern
+
## Process Overview
1. **Input**: A TypeScript source file containing `enum`, `type alias`, `interface`, and `function` declarations.
diff --git a/src/handlers/typebox/object/interface-type-handler.ts b/src/handlers/typebox/object/interface-type-handler.ts
index 8603c57..dfc4d02 100644
--- a/src/handlers/typebox/object/interface-type-handler.ts
+++ b/src/handlers/typebox/object/interface-type-handler.ts
@@ -1,6 +1,6 @@
import { ObjectLikeBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/object/object-like-base-handler'
import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
-import { HeritageClause, InterfaceDeclaration, Node, ts, TypeParameterDeclaration } from 'ts-morph'
+import { HeritageClause, InterfaceDeclaration, Node, ts } from 'ts-morph'
export class InterfaceTypeHandler extends ObjectLikeBaseHandler {
canHandle(node: Node): boolean {
@@ -12,11 +12,26 @@ export class InterfaceTypeHandler extends ObjectLikeBaseHandler {
const heritageClauses = node.getHeritageClauses()
const baseObjectType = this.createObjectType(this.processProperties(node.getProperties()))
- // If interface has type parameters, generate a function
+ // For generic interfaces, return raw TypeBox expression
+ // The parser will handle wrapping it in an arrow function using GenericTypeUtils
if (typeParameters.length > 0) {
- return this.createGenericInterfaceFunction(typeParameters, baseObjectType, heritageClauses)
+ // 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
}
@@ -33,64 +48,6 @@ export class InterfaceTypeHandler extends ObjectLikeBaseHandler {
return makeTypeCall('Composite', [ts.factory.createArrayLiteralExpression(allTypes, true)])
}
- private createGenericInterfaceFunction(
- typeParameters: TypeParameterDeclaration[],
- baseObjectType: ts.Expression,
- heritageClauses: HeritageClause[],
- ): ts.Expression {
- // Create function parameters for each type parameter
- const functionParams = typeParameters.map((typeParam) => {
- const paramName = typeParam.getName()
-
- return ts.factory.createParameterDeclaration(
- undefined,
- undefined,
- ts.factory.createIdentifier(paramName),
- undefined,
- ts.factory.createTypeReferenceNode(paramName, undefined),
- undefined,
- )
- })
-
- // Create function body
- let functionBody: ts.Expression = baseObjectType
-
- // Handle heritage clauses for generic interfaces
- const extendedTypes = this.collectExtendedTypes(heritageClauses)
-
- if (extendedTypes.length > 0) {
- const allTypes = [...extendedTypes, baseObjectType]
- functionBody = makeTypeCall('Composite', [
- ts.factory.createArrayLiteralExpression(allTypes, true),
- ])
- }
-
- // Create type parameters for the function
- const functionTypeParams = typeParameters.map((typeParam) => {
- const paramName = typeParam.getName()
-
- // Use TSchema as the constraint for TypeBox compatibility
- const constraintNode = ts.factory.createTypeReferenceNode('TSchema', undefined)
-
- return ts.factory.createTypeParameterDeclaration(
- undefined,
- ts.factory.createIdentifier(paramName),
- constraintNode,
- undefined,
- )
- })
-
- // Create arrow function
- return ts.factory.createArrowFunction(
- undefined,
- ts.factory.createNodeArray(functionTypeParams),
- functionParams,
- undefined,
- ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
- functionBody,
- )
- }
-
private parseGenericTypeCall(typeText: string): ts.Expression | null {
const match = typeText.match(/^([^<]+)<([^>]+)>$/)
diff --git a/src/handlers/typebox/readonly-array-type-handler.ts b/src/handlers/typebox/readonly-array-type-handler.ts
new file mode 100644
index 0000000..c09aeda
--- /dev/null
+++ b/src/handlers/typebox/readonly-array-type-handler.ts
@@ -0,0 +1,12 @@
+import { TypeOperatorBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/type-operator-base-handler'
+import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
+import { SyntaxKind, ts } from 'ts-morph'
+
+export class ReadonlyArrayTypeHandler extends TypeOperatorBaseHandler {
+ protected readonly operatorKind = SyntaxKind.ReadonlyKeyword
+ protected readonly typeBoxMethod = 'Readonly'
+
+ protected createTypeBoxCall(innerType: ts.Expression): ts.Expression {
+ return makeTypeCall('Readonly', [innerType])
+ }
+}
diff --git a/src/handlers/typebox/readonly-type-handler.ts b/src/handlers/typebox/readonly-type-handler.ts
index 745416c..e5b237b 100644
--- a/src/handlers/typebox/readonly-type-handler.ts
+++ b/src/handlers/typebox/readonly-type-handler.ts
@@ -1,12 +1,16 @@
-import { TypeOperatorBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/type-operator-base-handler'
+import { TypeReferenceBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/reference/type-reference-base-handler'
+import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call'
import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
-import { SyntaxKind, ts } from 'ts-morph'
+import { Node, ts } from 'ts-morph'
-export class ReadonlyTypeHandler extends TypeOperatorBaseHandler {
- protected readonly operatorKind = SyntaxKind.ReadonlyKeyword
- protected readonly typeBoxMethod = 'Readonly'
+export class ReadonlyTypeHandler extends TypeReferenceBaseHandler {
+ protected readonly supportedTypeNames = ['Readonly']
+ protected readonly expectedArgumentCount = 1
- protected createTypeBoxCall(innerType: ts.Expression): ts.Expression {
- return makeTypeCall('Readonly', [innerType])
+ handle(node: Node): ts.Expression {
+ const typeRef = this.validateTypeReference(node)
+ const [innerType] = this.extractTypeArguments(typeRef)
+
+ return makeTypeCall('Readonly', [getTypeBoxType(innerType)])
}
}
diff --git a/src/handlers/typebox/typebox-type-handlers.ts b/src/handlers/typebox/typebox-type-handlers.ts
index 89bc719..c96f603 100644
--- a/src/handlers/typebox/typebox-type-handlers.ts
+++ b/src/handlers/typebox/typebox-type-handlers.ts
@@ -9,6 +9,7 @@ import { KeyOfTypeHandler } from '@daxserver/validation-schema-codegen/handlers/
import { LiteralTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/literal-type-handler'
import { InterfaceTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/object/interface-type-handler'
import { ObjectTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/object/object-type-handler'
+import { ReadonlyArrayTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/readonly-array-type-handler'
import { ReadonlyTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/readonly-type-handler'
import { OmitTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/reference/omit-type-handler'
import { PartialTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/reference/partial-type-handler'
@@ -50,6 +51,7 @@ export class TypeBoxTypeHandlers {
const templateLiteralTypeHandler = new TemplateLiteralTypeHandler()
const typeofTypeHandler = new TypeofTypeHandler()
const readonlyTypeHandler = new ReadonlyTypeHandler()
+ const readonlyArrayTypeHandler = new ReadonlyArrayTypeHandler()
// O(1) lookup by SyntaxKind
this.syntaxKindHandlers.set(SyntaxKind.AnyKeyword, simpleTypeHandler)
@@ -80,13 +82,14 @@ export class TypeBoxTypeHandlers {
this.typeReferenceHandlers.set('Pick', pickTypeHandler)
this.typeReferenceHandlers.set('Omit', omitTypeHandler)
this.typeReferenceHandlers.set('Required', requiredTypeHandler)
+ this.typeReferenceHandlers.set('Readonly', readonlyTypeHandler)
// Fallback handlers for complex cases
this.fallbackHandlers = [
typeReferenceHandler,
keyOfTypeHandler,
typeofTypeHandler,
- readonlyTypeHandler,
+ readonlyArrayTypeHandler,
]
}
diff --git a/src/index.ts b/src/index.ts
index ea4644e..c7c2d6a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -14,8 +14,11 @@ const createOutputFile = (hasGenericInterfaces: boolean) => {
})
// Add imports
- const namedImports = [
- 'Type',
+ const namedImports: { name: string; isTypeOnly: boolean }[] = [
+ {
+ name: 'Type',
+ isTypeOnly: false,
+ },
{
name: 'Static',
isTypeOnly: true,
@@ -75,9 +78,11 @@ export const generateCode = (options: InputOptions): string => {
const dependencyTraversal = new DependencyTraversal()
const traversedNodes = dependencyTraversal.startTraversal(sourceFile)
- // Check if any interfaces have generic type parameters
+ // Check if any interfaces or type aliases have generic type parameters
const hasGenericInterfaces = traversedNodes.some(
- (t) => Node.isInterfaceDeclaration(t.node) && t.node.getTypeParameters().length > 0,
+ (t) =>
+ (Node.isInterfaceDeclaration(t.node) && t.node.getTypeParameters().length > 0) ||
+ (Node.isTypeAliasDeclaration(t.node) && t.node.getTypeParameters().length > 0),
)
// Create output file with proper imports
diff --git a/src/parsers/parse-interfaces.ts b/src/parsers/parse-interfaces.ts
index 46a77e2..4143629 100644
--- a/src/parsers/parse-interfaces.ts
+++ b/src/parsers/parse-interfaces.ts
@@ -1,12 +1,8 @@
import { BaseParser } from '@daxserver/validation-schema-codegen/parsers/base-parser'
import { addStaticTypeAlias } from '@daxserver/validation-schema-codegen/utils/add-static-type-alias'
+import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils'
import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call'
-import {
- InterfaceDeclaration,
- ts,
- TypeParameterDeclaration,
- VariableDeclarationKind,
-} from 'ts-morph'
+import { InterfaceDeclaration, ts } from 'ts-morph'
export class InterfaceParser extends BaseParser {
parse(interfaceDecl: InterfaceDeclaration): void {
@@ -39,16 +35,7 @@ export class InterfaceParser extends BaseParser {
this.newSourceFile.compilerNode,
)
- this.newSourceFile.addVariableStatement({
- isExported: true,
- declarationKind: VariableDeclarationKind.Const,
- declarations: [
- {
- name: interfaceName,
- initializer: typeboxType,
- },
- ],
- })
+ GenericTypeUtils.addTypeBoxVariableStatement(this.newSourceFile, interfaceName, typeboxType)
addStaticTypeAlias(
this.newSourceFile,
@@ -62,85 +49,34 @@ export class InterfaceParser extends BaseParser {
const interfaceName = interfaceDecl.getName()
const typeParameters = interfaceDecl.getTypeParameters()
- // Generate TypeBox function definition
+ // Generate TypeBox function definition using the same flow as type aliases
const typeboxTypeNode = getTypeBoxType(interfaceDecl)
- const typeboxType = this.printer.printNode(
- ts.EmitHint.Expression,
- typeboxTypeNode,
- this.newSourceFile.compilerNode,
- )
-
- // Add the function declaration
- this.newSourceFile.addVariableStatement({
- isExported: true,
- declarationKind: VariableDeclarationKind.Const,
- declarations: [
- {
- name: interfaceName,
- initializer: typeboxType,
- },
- ],
- })
-
- // Add generic type alias: type A = Static>>
- this.addGenericTypeAlias(interfaceName, typeParameters)
- }
-
- private addGenericTypeAlias(name: string, typeParameters: TypeParameterDeclaration[]): void {
- // Create type parameters for the type alias
- const typeParamDeclarations = typeParameters.map((typeParam) => {
- const paramName = typeParam.getName()
- // Use TSchema as the constraint for TypeBox compatibility
- const constraintNode = ts.factory.createTypeReferenceNode('TSchema', undefined)
-
- return ts.factory.createTypeParameterDeclaration(
- undefined,
- ts.factory.createIdentifier(paramName),
- constraintNode,
- undefined,
- )
- })
-
- // Create the type: Static>>
- const typeParamNames = typeParameters.map((tp) => tp.getName())
- const typeArguments = typeParamNames.map((paramName) =>
- ts.factory.createTypeReferenceNode(paramName, undefined),
- )
- // Create typeof A expression - we need to create a type reference with type arguments
- const typeReferenceWithArgs = ts.factory.createTypeReferenceNode(
- ts.factory.createIdentifier(name),
- typeArguments,
- )
-
- const typeofExpression = ts.factory.createTypeQueryNode(
- typeReferenceWithArgs.typeName,
- typeReferenceWithArgs.typeArguments,
+ // Create the function expression using shared utilities (mirrors type-alias flow)
+ const functionExpression = GenericTypeUtils.createGenericArrowFunction(
+ typeParameters,
+ typeboxTypeNode,
)
- const returnTypeExpression = ts.factory.createTypeReferenceNode(
- ts.factory.createIdentifier('ReturnType'),
- [typeofExpression],
+ const functionExpressionText = this.printer.printNode(
+ ts.EmitHint.Expression,
+ functionExpression,
+ this.newSourceFile.compilerNode,
)
- const staticTypeNode = ts.factory.createTypeReferenceNode(
- ts.factory.createIdentifier('Static'),
- [returnTypeExpression],
+ // Add the function declaration
+ GenericTypeUtils.addTypeBoxVariableStatement(
+ this.newSourceFile,
+ interfaceName,
+ functionExpressionText,
)
- const staticType = this.printer.printNode(
- ts.EmitHint.Unspecified,
- staticTypeNode,
- this.newSourceFile.compilerNode,
+ // Add generic type alias using shared utility
+ GenericTypeUtils.addGenericTypeAlias(
+ this.newSourceFile,
+ interfaceName,
+ typeParameters,
+ this.printer,
)
-
- this.newSourceFile.addTypeAlias({
- isExported: true,
- name,
- typeParameters: typeParamDeclarations.map((tp) =>
- this.printer.printNode(ts.EmitHint.Unspecified, tp, this.newSourceFile.compilerNode),
- ),
- type: staticType,
- })
}
}
diff --git a/src/parsers/parse-type-aliases.ts b/src/parsers/parse-type-aliases.ts
index 9fd5695..0c7fe74 100644
--- a/src/parsers/parse-type-aliases.ts
+++ b/src/parsers/parse-type-aliases.ts
@@ -1,15 +1,31 @@
import { BaseParser } from '@daxserver/validation-schema-codegen/parsers/base-parser'
import { addStaticTypeAlias } from '@daxserver/validation-schema-codegen/utils/add-static-type-alias'
+import { GenericTypeUtils } from '@daxserver/validation-schema-codegen/utils/generic-type-utils'
import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call'
import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
-import { ts, TypeAliasDeclaration, VariableDeclarationKind } from 'ts-morph'
+import { ts, TypeAliasDeclaration } from 'ts-morph'
export class TypeAliasParser extends BaseParser {
parse(typeAlias: TypeAliasDeclaration): void {
- this.parseWithImportFlag(typeAlias)
+ const typeName = typeAlias.getName()
+
+ if (this.processedTypes.has(typeName)) {
+ return
+ }
+
+ this.processedTypes.add(typeName)
+
+ const typeParameters = typeAlias.getTypeParameters()
+
+ // Check if type alias has type parameters (generic)
+ if (typeParameters.length > 0) {
+ this.parseGenericTypeAlias(typeAlias)
+ } else {
+ this.parseRegularTypeAlias(typeAlias)
+ }
}
- parseWithImportFlag(typeAlias: TypeAliasDeclaration): void {
+ private parseRegularTypeAlias(typeAlias: TypeAliasDeclaration): void {
const typeName = typeAlias.getName()
const typeNode = typeAlias.getTypeNode()
@@ -20,17 +36,39 @@ export class TypeAliasParser extends BaseParser {
this.newSourceFile.compilerNode,
)
- this.newSourceFile.addVariableStatement({
- isExported: true,
- declarationKind: VariableDeclarationKind.Const,
- declarations: [
- {
- name: typeName,
- initializer: typeboxType,
- },
- ],
- })
+ GenericTypeUtils.addTypeBoxVariableStatement(this.newSourceFile, typeName, typeboxType)
addStaticTypeAlias(this.newSourceFile, typeName, this.newSourceFile.compilerNode, this.printer)
}
+
+ private parseGenericTypeAlias(typeAlias: TypeAliasDeclaration): void {
+ const typeName = typeAlias.getName()
+ const typeParameters = typeAlias.getTypeParameters()
+
+ // Generate TypeBox function definition
+ const typeNode = typeAlias.getTypeNode()
+ const typeboxTypeNode = typeNode ? getTypeBoxType(typeNode) : makeTypeCall('Any')
+
+ // Create the function expression using shared utilities
+ const functionExpression = GenericTypeUtils.createGenericArrowFunction(
+ typeParameters,
+ typeboxTypeNode,
+ )
+
+ const functionExpressionText = this.printer.printNode(
+ ts.EmitHint.Expression,
+ functionExpression,
+ this.newSourceFile.compilerNode,
+ )
+
+ // Add the function declaration
+ GenericTypeUtils.addTypeBoxVariableStatement(
+ this.newSourceFile,
+ typeName,
+ functionExpressionText,
+ )
+
+ // Add generic type alias using shared utility
+ GenericTypeUtils.addGenericTypeAlias(this.newSourceFile, typeName, typeParameters, this.printer)
+ }
}
diff --git a/src/printer/typebox-printer.ts b/src/printer/typebox-printer.ts
index 034fb55..e935fc1 100644
--- a/src/printer/typebox-printer.ts
+++ b/src/printer/typebox-printer.ts
@@ -41,7 +41,7 @@ export class TypeBoxPrinter {
switch (true) {
case Node.isTypeAliasDeclaration(node):
- this.typeAliasParser.parseWithImportFlag(node)
+ this.typeAliasParser.parse(node)
break
case Node.isInterfaceDeclaration(node):
diff --git a/src/utils/generic-type-utils.ts b/src/utils/generic-type-utils.ts
new file mode 100644
index 0000000..6a4ea9d
--- /dev/null
+++ b/src/utils/generic-type-utils.ts
@@ -0,0 +1,148 @@
+import { SourceFile, ts, TypeParameterDeclaration, VariableDeclarationKind } from 'ts-morph'
+
+/**
+ * Utility functions for handling generic types in parsers
+ */
+export class GenericTypeUtils {
+ /**
+ * Adds a TypeBox variable statement to the source file
+ */
+ static addTypeBoxVariableStatement(
+ newSourceFile: SourceFile,
+ name: string,
+ initializer: string,
+ ): void {
+ newSourceFile.addVariableStatement({
+ isExported: true,
+ declarationKind: VariableDeclarationKind.Const,
+ declarations: [
+ {
+ name,
+ initializer,
+ },
+ ],
+ })
+ }
+ /**
+ * Creates function parameters for generic type parameters
+ */
+ static createFunctionParameters(
+ typeParameters: TypeParameterDeclaration[],
+ ): ts.ParameterDeclaration[] {
+ return typeParameters.map((typeParam) => {
+ const paramName = typeParam.getName()
+
+ return ts.factory.createParameterDeclaration(
+ undefined,
+ undefined,
+ ts.factory.createIdentifier(paramName),
+ undefined,
+ ts.factory.createTypeReferenceNode(paramName),
+ )
+ })
+ }
+
+ /**
+ * Creates type parameters with TSchema constraints for generic functions
+ */
+ static createFunctionTypeParameters(
+ typeParameters: TypeParameterDeclaration[],
+ ): ts.TypeParameterDeclaration[] {
+ return typeParameters.map((typeParam) => {
+ const paramName = typeParam.getName()
+ const constraintNode = ts.factory.createTypeReferenceNode('TSchema')
+
+ return ts.factory.createTypeParameterDeclaration(
+ undefined,
+ ts.factory.createIdentifier(paramName),
+ constraintNode,
+ )
+ })
+ }
+
+ /**
+ * Creates an arrow function for generic types
+ */
+ static createGenericArrowFunction(
+ typeParameters: TypeParameterDeclaration[],
+ functionBody: ts.Expression,
+ ): ts.Expression {
+ const functionParams = this.createFunctionParameters(typeParameters)
+ const functionTypeParams = this.createFunctionTypeParameters(typeParameters)
+
+ return ts.factory.createArrowFunction(
+ undefined,
+ functionTypeParams,
+ functionParams,
+ undefined,
+ ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
+ functionBody,
+ )
+ }
+
+ /**
+ * Adds a generic type alias to the source file
+ * Generates: export type TypeName = Static>>
+ */
+ static addGenericTypeAlias(
+ newSourceFile: SourceFile,
+ name: string,
+ typeParameters: TypeParameterDeclaration[],
+ printer: ts.Printer,
+ ): void {
+ // Create type parameters for the type alias
+ const typeParamDeclarations = typeParameters.map((typeParam) => {
+ const paramName = typeParam.getName()
+ // Use TSchema as the constraint for TypeBox compatibility
+ const constraintNode = ts.factory.createTypeReferenceNode('TSchema')
+
+ return ts.factory.createTypeParameterDeclaration(
+ undefined,
+ ts.factory.createIdentifier(paramName),
+ constraintNode,
+ )
+ })
+
+ // Create the type: Static>>
+ const typeParamNames = typeParameters.map((tp) => tp.getName())
+ const typeArguments = typeParamNames.map((paramName) =>
+ ts.factory.createTypeReferenceNode(paramName),
+ )
+
+ // Create typeof A expression - we need to create a type reference with type arguments
+ const typeReferenceWithArgs = ts.factory.createTypeReferenceNode(
+ ts.factory.createIdentifier(name),
+ typeArguments,
+ )
+
+ const typeofExpression = ts.factory.createTypeQueryNode(
+ typeReferenceWithArgs.typeName,
+ typeReferenceWithArgs.typeArguments,
+ )
+
+ const returnTypeExpression = ts.factory.createTypeReferenceNode(
+ ts.factory.createIdentifier('ReturnType'),
+ [typeofExpression],
+ )
+
+ const staticTypeNode = ts.factory.createTypeReferenceNode(
+ ts.factory.createIdentifier('Static'),
+ [returnTypeExpression],
+ )
+
+ const staticType = printer.printNode(
+ ts.EmitHint.Unspecified,
+ staticTypeNode,
+ newSourceFile.compilerNode,
+ )
+
+ newSourceFile.addTypeAlias({
+ isExported: true,
+ name,
+ typeParameters: typeParamDeclarations.map((tp) =>
+ printer.printNode(ts.EmitHint.Unspecified, tp, newSourceFile.compilerNode),
+ ),
+ type: staticType,
+ })
+ }
+}
diff --git a/src/utils/key-extraction-utils.ts b/src/utils/key-extraction-utils.ts
index 01109d7..94d896f 100644
--- a/src/utils/key-extraction-utils.ts
+++ b/src/utils/key-extraction-utils.ts
@@ -39,7 +39,6 @@ export const createTypeBoxKeys = (keys: string[]): ts.Expression => {
return makeTypeCall('Union', [
ts.factory.createArrayLiteralExpression(
keys.map((k) => makeTypeCall('Literal', [ts.factory.createStringLiteral(k)])),
- false,
),
])
}
diff --git a/tests/handlers/typebox/generics.test.ts b/tests/handlers/typebox/generics.test.ts
new file mode 100644
index 0000000..cd1becd
--- /dev/null
+++ b/tests/handlers/typebox/generics.test.ts
@@ -0,0 +1,170 @@
+import { createSourceFile, formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils'
+import { beforeEach, describe, expect, test } from 'bun:test'
+import { Project } from 'ts-morph'
+
+describe('generic types', () => {
+ let project: Project
+
+ beforeEach(() => {
+ project = new Project()
+ })
+
+ test('generic types', () => {
+ const sourceFile = createSourceFile(
+ project,
+ `
+ interface A { a: T }
+ interface B extends A { b: number }
+ `,
+ )
+
+ expect(generateFormattedCode(sourceFile)).toBe(
+ formatWithPrettier(
+ `
+ export const A = (T: T) => Type.Object({
+ a: T
+ });
+
+ export type A = Static>>;
+
+ export const B = Type.Composite([A(Type.Number()), Type.Object({
+ b: Type.Number()
+ })]);
+
+ export type B = Static;
+ `,
+ true,
+ true,
+ ),
+ )
+ })
+
+ test('generic types extension', () => {
+ const sourceFile = createSourceFile(
+ project,
+ `
+ interface A { a: T }
+ interface B extends A { b: T }
+ `,
+ )
+
+ expect(generateFormattedCode(sourceFile)).toBe(
+ formatWithPrettier(
+ `
+ export const A = (T: T) => Type.Object({
+ a: T
+ });
+
+ export type A = Static>>;
+
+ export const B = (T: T) => Type.Composite([A(T), Type.Object({
+ b: T
+ })]);
+
+ export type B = Static>>;
+ `,
+ true,
+ true,
+ ),
+ )
+ })
+
+ test('generic types with extended type', () => {
+ const sourceFile = createSourceFile(
+ project,
+ `
+ declare const A: readonly ["a", "b"]
+ type A = typeof A[number]
+ interface B { a: T }
+ type C = B<'a'>
+ type D = B<'b'>
+ `,
+ )
+
+ expect(generateFormattedCode(sourceFile)).toBe(
+ formatWithPrettier(
+ `
+ export const A = Type.Union([Type.Literal('a'), Type.Literal('b')])
+
+ export type A = Static
+
+ export const B = (T: T) => Type.Object({
+ a: T
+ })
+
+ export type B = Static>>
+
+ export const C = B(Type.Literal('a'))
+
+ export type C = Static
+
+ export const D = B(Type.Literal('b'))
+
+ export type D = Static
+ `,
+ true,
+ true,
+ ),
+ )
+ })
+
+ test('generics with complexity', () => {
+ const sourceFile = createSourceFile(
+ project,
+ `
+ export type LanguageCode = string;
+ type LanguageRecord = Partial>>;
+ `,
+ )
+
+ expect(generateFormattedCode(sourceFile)).toBe(
+ formatWithPrettier(
+ `
+ export const LanguageCode = Type.String()
+
+ export type LanguageCode = Static
+
+ export const LanguageRecord = (V: V) => Type.Partial(Type.Readonly(Type.Record(LanguageCode, V)))
+
+ export type LanguageRecord = Static>>
+ `,
+ true,
+ true,
+ ),
+ )
+ })
+
+ test('multiple generic parameters with constraints', () => {
+ const sourceFile = createSourceFile(
+ project,
+ `
+ type ApiResponse = {
+ data?: T;
+ error?: E;
+ status: number;
+ };
+ type UserResponse = ApiResponse;
+ `,
+ )
+
+ expect(generateFormattedCode(sourceFile)).toBe(
+ formatWithPrettier(
+ `
+ export const ApiResponse = (T: T, E: E) => Type.Object({
+ data: Type.Optional(T),
+ error: Type.Optional(E),
+ status: Type.Number()
+ })
+
+ export type ApiResponse = Static>>
+
+ export const UserResponse = (T: T) => ApiResponse(T, Type.String())
+
+ export type UserResponse = Static>>
+ `,
+ true,
+ true,
+ ),
+ )
+ })
+})
diff --git a/tests/handlers/typebox/interface-generics-consistency.test.ts b/tests/handlers/typebox/interface-generics-consistency.test.ts
new file mode 100644
index 0000000..6a08912
--- /dev/null
+++ b/tests/handlers/typebox/interface-generics-consistency.test.ts
@@ -0,0 +1,97 @@
+import { createSourceFile, formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils'
+import { beforeEach, describe, expect, test } from 'bun:test'
+import { Project } from 'ts-morph'
+
+describe('Interface Generic Consistency with Type Aliases', () => {
+ let project: Project
+
+ beforeEach(() => {
+ project = new Project()
+ })
+
+ test('interface and type alias should generate identical patterns for generics', () => {
+ // Test that both interface and type alias generate the same arrow function pattern
+ const interfaceSource = createSourceFile(
+ project,
+ `
+ interface Container {
+ value: T;
+ id: string;
+ }
+ `,
+ )
+
+ const typeAliasSource = createSourceFile(
+ project,
+ `
+ type Container = {
+ value: T;
+ id: string;
+ }
+ `,
+ 'type-alias.ts',
+ )
+
+ const interfaceResult = generateFormattedCode(interfaceSource, true)
+ const typeAliasResult = generateFormattedCode(typeAliasSource, true)
+
+ // Both should generate the same arrow function pattern
+ const expectedPattern = formatWithPrettier(
+ `
+ export const Container = (T: T) => Type.Object({
+ value: T,
+ id: Type.String(),
+ });
+
+ export type Container = Static>>;
+ `,
+ true,
+ true,
+ )
+
+ expect(interfaceResult).toBe(expectedPattern)
+ expect(typeAliasResult).toBe(expectedPattern)
+ })
+
+ test('complex generic interface should use GenericTypeUtils flow', () => {
+ // This test is designed to fail if the interface parser doesn't use
+ // the same GenericTypeUtils.createGenericArrowFunction flow as type aliases
+ const sourceFile = createSourceFile(
+ project,
+ `
+ interface ApiResponse {
+ data: T;
+ error: E;
+ status: number;
+ metadata: {
+ timestamp: string;
+ version: number;
+ };
+ }
+ `,
+ )
+
+ const result = generateFormattedCode(sourceFile, true)
+
+ // Should generate using the same pattern as type aliases
+ expect(result).toBe(
+ formatWithPrettier(
+ `
+ export const ApiResponse = (T: T, E: E) => Type.Object({
+ data: T,
+ error: E,
+ status: Type.Number(),
+ metadata: Type.Object({
+ timestamp: Type.String(),
+ version: Type.Number(),
+ }),
+ });
+
+ export type ApiResponse = Static>>;
+ `,
+ true,
+ true,
+ ),
+ )
+ })
+})
diff --git a/tests/handlers/typebox/interface-generics-runtime-binding.test.ts b/tests/handlers/typebox/interface-generics-runtime-binding.test.ts
new file mode 100644
index 0000000..7a689fa
--- /dev/null
+++ b/tests/handlers/typebox/interface-generics-runtime-binding.test.ts
@@ -0,0 +1,118 @@
+import { createSourceFile, formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils'
+import { beforeEach, describe, expect, test } from 'bun:test'
+import { Project } from 'ts-morph'
+
+describe('Interface Generic Runtime Binding', () => {
+ let project: Project
+
+ beforeEach(() => {
+ project = new Project()
+ })
+
+ test('generic interface should generate arrow function wrapper for runtime bindings', () => {
+ const sourceFile = createSourceFile(
+ project,
+ `
+ interface Container {
+ value: T;
+ id: string;
+ }
+ `,
+ )
+
+ const result = generateFormattedCode(sourceFile, true)
+
+ // The generated code should be an arrow function that takes type parameters
+ // and returns the TypeBox expression, not just the raw TypeBox expression
+ expect(result).toBe(
+ formatWithPrettier(
+ `
+ export const Container = (T: T) => Type.Object({
+ value: T,
+ id: Type.String(),
+ });
+
+ export type Container = Static>>;
+ `,
+ true,
+ true,
+ ),
+ )
+ })
+
+ test('generic interface with multiple type parameters should generate proper arrow function', () => {
+ const sourceFile = createSourceFile(
+ project,
+ `
+ interface Response {
+ data: T;
+ error: E;
+ timestamp: number;
+ }
+ `,
+ )
+
+ const result = generateFormattedCode(sourceFile, true)
+
+ expect(result).toBe(
+ formatWithPrettier(
+ `
+ export const Response = (T: T, E: E) => Type.Object({
+ data: T,
+ error: E,
+ timestamp: Type.Number(),
+ });
+
+ export type Response = Static>>;
+ `,
+ true,
+ true,
+ ),
+ )
+ })
+
+ test('should fail with current implementation - demonstrates the issue', () => {
+ // This test is designed to fail with the current implementation
+ // to show that we need to fix the generic interface handling
+ const sourceFile = createSourceFile(
+ project,
+ `
+ interface GenericContainer {
+ first: T;
+ second: U;
+ metadata: {
+ created: string;
+ updated: string;
+ };
+ }
+ `,
+ )
+
+ const result = generateFormattedCode(sourceFile, true)
+
+ // This should generate an arrow function, but if the current implementation
+ // is broken, it might generate something like:
+ // export const GenericContainer = Type.Object({...})
+ // instead of:
+ // export const GenericContainer = (T: T, U: U) => Type.Object({...})
+
+ expect(result).toBe(
+ formatWithPrettier(
+ `
+ export const GenericContainer = (T: T, U: U) => Type.Object({
+ first: T,
+ second: U,
+ metadata: Type.Object({
+ created: Type.String(),
+ updated: Type.String(),
+ }),
+ });
+
+ export type GenericContainer = Static>>;
+ `,
+ true,
+ true,
+ ),
+ )
+ })
+})
diff --git a/tests/handlers/typebox/interfaces.test.ts b/tests/handlers/typebox/interfaces.test.ts
index 4a1c697..e813622 100644
--- a/tests/handlers/typebox/interfaces.test.ts
+++ b/tests/handlers/typebox/interfaces.test.ts
@@ -200,106 +200,5 @@ describe('Interfaces', () => {
`),
)
})
-
- describe('generic types', () => {
- test('generic types', () => {
- const sourceFile = createSourceFile(
- project,
- `
- interface A { a: T }
- interface B extends A { b: number }
- `,
- )
-
- expect(generateFormattedCode(sourceFile)).toBe(
- formatWithPrettier(
- `
- export const A = (T: T) => Type.Object({
- a: T
- });
-
- export type A = Static>>;
-
- export const B = Type.Composite([A(Type.Number()), Type.Object({
- b: Type.Number()
- })]);
-
- export type B = Static;
- `,
- true,
- true,
- ),
- )
- })
-
- test('generic types extension', () => {
- const sourceFile = createSourceFile(
- project,
- `
- interface A { a: T }
- interface B extends A { b: T }
- `,
- )
-
- expect(generateFormattedCode(sourceFile)).toBe(
- formatWithPrettier(
- `
- export const A = (T: T) => Type.Object({
- a: T
- });
-
- export type A = Static>>;
-
- export const B = (T: T) => Type.Composite([A(T), Type.Object({
- b: T
- })]);
-
- export type B = Static>>;
- `,
- true,
- true,
- ),
- )
- })
-
- test('generic types with extended type', () => {
- const sourceFile = createSourceFile(
- project,
- `
- declare const A: readonly ["a", "b"]
- type A = typeof A[number]
- interface B { a: T }
- type C = B<'a'>
- type D = B<'b'>
- `,
- )
-
- expect(generateFormattedCode(sourceFile)).toBe(
- formatWithPrettier(
- `
- export const A = Type.Union([Type.Literal('a'), Type.Literal('b')])
-
- export type A = Static
-
- export const B = (T: T) => Type.Object({
- a: T
- })
-
- export type B = Static>>
-
- export const C = B(Type.Literal('a'))
-
- export type C = Static
-
- export const D = B(Type.Literal('b'))
-
- export type D = Static
- `,
- true,
- true,
- ),
- )
- })
- })
})
})
diff --git a/tests/handlers/typebox/readonly.test.ts b/tests/handlers/typebox/readonly.test.ts
new file mode 100644
index 0000000..a31c0c9
--- /dev/null
+++ b/tests/handlers/typebox/readonly.test.ts
@@ -0,0 +1,153 @@
+import { createSourceFile, formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils'
+import { beforeEach, describe, expect, test } from 'bun:test'
+import { Project } from 'ts-morph'
+
+describe('Readonly types', () => {
+ let project: Project
+
+ beforeEach(() => {
+ project = new Project()
+ })
+
+ describe('Readonly utility type', () => {
+ test('simple readonly object', () => {
+ const sourceFile = createSourceFile(project, 'type Test = Readonly<{ a: string }>')
+
+ expect(generateFormattedCode(sourceFile)).toBe(
+ formatWithPrettier(
+ `
+ export const Test = Type.Readonly(Type.Object({
+ a: Type.String()
+ }))
+
+ export type Test = Static
+ `,
+ ),
+ )
+ })
+
+ test('nested readonly types', () => {
+ const sourceFile = createSourceFile(
+ project,
+ 'type Test = Partial>>',
+ )
+
+ expect(generateFormattedCode(sourceFile)).toBe(
+ formatWithPrettier(
+ `
+ export const Test = Type.Partial(Type.Readonly(Type.Record(Type.String(), Type.Number())))
+
+ export type Test = Static
+ `,
+ ),
+ )
+ })
+
+ test('generic readonly type', () => {
+ const sourceFile = createSourceFile(project, 'type Test = Readonly')
+
+ expect(generateFormattedCode(sourceFile)).toBe(
+ formatWithPrettier(
+ `
+ export const Test = (T: T) => Type.Readonly(T)
+
+ export type Test = Static>>
+ `,
+ true,
+ true,
+ ),
+ )
+ })
+ })
+
+ describe('Readonly array modifier', () => {
+ test('readonly string array', () => {
+ const sourceFile = createSourceFile(project, 'type Test = readonly string[]')
+
+ expect(generateFormattedCode(sourceFile)).toBe(
+ formatWithPrettier(
+ `
+ export const Test = Type.Readonly(Type.Array(Type.String()))
+
+ export type Test = Static
+ `,
+ ),
+ )
+ })
+
+ test('readonly custom type array', () => {
+ const sourceFile = createSourceFile(
+ project,
+ `
+ type CustomType = { id: string }
+ type Test = readonly CustomType[]
+ `,
+ )
+
+ expect(generateFormattedCode(sourceFile)).toBe(
+ formatWithPrettier(
+ `
+ export const CustomType = Type.Object({
+ id: Type.String()
+ })
+
+ export type CustomType = Static
+
+ export const Test = Type.Readonly(Type.Array(CustomType))
+
+ export type Test = Static
+ `,
+ ),
+ )
+ })
+
+ test('readonly tuple', () => {
+ const sourceFile = createSourceFile(project, 'type Test = readonly [string, number]')
+
+ expect(generateFormattedCode(sourceFile)).toBe(
+ formatWithPrettier(
+ `
+ export const Test = Type.Readonly(Type.Tuple([Type.String(), Type.Number()]))
+
+ export type Test = Static
+ `,
+ ),
+ )
+ })
+ })
+
+ describe('Mixed readonly scenarios', () => {
+ test('readonly array inside readonly utility type', () => {
+ const sourceFile = createSourceFile(
+ project,
+ 'type Test = Readonly<{ items: readonly string[] }>',
+ )
+
+ expect(generateFormattedCode(sourceFile)).toBe(
+ formatWithPrettier(
+ `
+ export const Test = Type.Readonly(Type.Object({
+ items: Type.Readonly(Type.Array(Type.String()))
+ }))
+
+ export type Test = Static
+ `,
+ ),
+ )
+ })
+
+ test('readonly utility type with array', () => {
+ const sourceFile = createSourceFile(project, 'type Test = Readonly')
+
+ expect(generateFormattedCode(sourceFile)).toBe(
+ formatWithPrettier(
+ `
+ export const Test = Type.Readonly(Type.Array(Type.String()))
+
+ export type Test = Static
+ `,
+ ),
+ )
+ })
+ })
+})