Skip to content

Commit df05d59

Browse files
authored
feat(type-aliases): add support for generic type aliases (#15)
The changes in this commit add support for parsing and generating code for generic type aliases in the validation schema codegen tool. The main changes are: 1. Modify the `index.ts` file to check if any interfaces or type aliases have generic type parameters. 2. Implement the `parseGenericTypeAlias` method in the `TypeAliasParser` class to handle the parsing and code generation for generic type aliases. 3. Add a new method `createGenericTypeAliasFunction` to generate the TypeBox function definition for the generic type alias. 4. Implement the `addGenericTypeAlias` method to add the generic type alias declaration to the output file. These changes allow the validation schema codegen tool to properly handle and generate code for generic type aliases, which is an important feature for users who need to define complex schema types.
1 parent 6075948 commit df05d59

16 files changed

+892
-288
lines changed

ARCHITECTURE.md

Lines changed: 77 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
### Supported TypeScript Constructs
2525

2626
- **Type Definitions**: Type aliases, interfaces, enums, and function declarations
27-
- **Generic Types**: Generic interfaces and type parameters with proper constraint handling
27+
- **Generic Types**: Generic interfaces and type aliases with type parameters and proper constraint handling
2828
- **Complex Types**: Union and intersection types, nested object structures, template literal types
29-
- **Utility Types**: Built-in support for Pick, Omit, Partial, Required, Record, and other TypeScript utility types
29+
- **Utility Types**: Built-in support for Pick, Omit, Partial, Required, Record, Readonly, and other TypeScript utility types
3030
- **Advanced Features**: Conditional types, mapped types, keyof operators, indexed access types
3131
- **Import Resolution**: Cross-file type dependencies with qualified naming and circular dependency handling
3232

@@ -39,7 +39,7 @@ The main logic for code generation resides in the <mcfile name="index.ts" path="
3939
The `generateCode` function in <mcfile name="index.ts" path="src/index.ts"></mcfile> orchestrates the entire code generation process:
4040

4141
1. **Input Processing**: Creates a `SourceFile` from input using `createSourceFileFromInput`
42-
2. **Generic Interface Detection**: Checks for generic interfaces to determine required TypeBox imports
42+
2. **Generic Type Detection**: Checks for generic interfaces and type aliases to determine required TypeBox imports (including `TSchema`)
4343
3. **Output File Creation**: Creates a new output file with necessary `@sinclair/typebox` imports using `createOutputFile`
4444
4. **Dependency Traversal**: Uses `DependencyTraversal` to analyze and sort all type dependencies
4545
5. **Code Generation**: Processes sorted nodes using `TypeBoxPrinter` in `printSortedNodes`
@@ -113,8 +113,8 @@ The <mcfile name="base-parser.ts" path="src/parsers/base-parser.ts"></mcfile> pr
113113

114114
#### Specialized Parsers
115115

116-
1. **InterfaceParser**: <mcfile name="parse-interfaces.ts" path="src/parsers/parse-interfaces.ts"></mcfile> - Handles both regular and generic interfaces
117-
2. **TypeAliasParser**: <mcfile name="parse-type-aliases.ts" path="src/parsers/parse-type-aliases.ts"></mcfile> - Processes type alias declarations
116+
1. **InterfaceParser**: <mcfile name="parse-interfaces.ts" path="src/parsers/parse-interfaces.ts"></mcfile> - Handles both regular and generic interfaces using the unified `GenericTypeUtils` flow for consistency with type aliases
117+
2. **TypeAliasParser**: <mcfile name="parse-type-aliases.ts" path="src/parsers/parse-type-aliases.ts"></mcfile> - Processes both regular and generic type alias declarations using `GenericTypeUtils.createGenericArrowFunction`
118118
3. **EnumParser**: <mcfile name="parse-enums.ts" path="src/parsers/parse-enums.ts"></mcfile> - Handles enum declarations
119119
4. **FunctionParser**: <mcfile name="parse-function-declarations.ts" path="src/parsers/parse-function-declarations.ts"></mcfile> - Processes function declarations
120120
5. **ImportParser**: <mcfile name="parse-imports.ts" path="src/parsers/parse-imports.ts"></mcfile> - Handles import resolution
@@ -127,20 +127,42 @@ The handler system in <mcfile name="handlers/typebox" path="src/handlers/typebox
127127

128128
1. **Base Handlers**: Foundation classes including <mcfile name="base-type-handler.ts" path="src/handlers/typebox/base-type-handler.ts"></mcfile> and specialized base classes
129129
2. **Collection Handlers**: <mcfile name="array-type-handler.ts" path="src/handlers/typebox/collection/array-type-handler.ts"></mcfile>, <mcfile name="intersection-type-handler.ts" path="src/handlers/typebox/collection/intersection-type-handler.ts"></mcfile>, <mcfile name="tuple-type-handler.ts" path="src/handlers/typebox/collection/tuple-type-handler.ts"></mcfile>, <mcfile name="union-type-handler.ts" path="src/handlers/typebox/collection/union-type-handler.ts"></mcfile>
130-
3. **Object Handlers**: <mcfile name="interface-type-handler.ts" path="src/handlers/typebox/object/interface-type-handler.ts"></mcfile>, <mcfile name="object-type-handler.ts" path="src/handlers/typebox/object/object-type-handler.ts"></mcfile>
130+
3. **Object Handlers**: <mcfile name="interface-type-handler.ts" path="src/handlers/typebox/object/interface-type-handler.ts"></mcfile> (returns raw TypeBox expressions for generic interfaces, allowing parser-level arrow function wrapping), <mcfile name="object-type-handler.ts" path="src/handlers/typebox/object/object-type-handler.ts"></mcfile>
131131
4. **Reference Handlers**: <mcfile name="omit-type-handler.ts" path="src/handlers/typebox/reference/omit-type-handler.ts"></mcfile>, <mcfile name="partial-type-handler.ts" path="src/handlers/typebox/reference/partial-type-handler.ts"></mcfile>, <mcfile name="pick-type-handler.ts" path="src/handlers/typebox/reference/pick-type-handler.ts"></mcfile>, <mcfile name="record-type-handler.ts" path="src/handlers/typebox/reference/record-type-handler.ts"></mcfile>, <mcfile name="required-type-handler.ts" path="src/handlers/typebox/reference/required-type-handler.ts"></mcfile>
132-
5. **Simple Handlers**: <mcfile name="simple-type-handler.ts" path="src/handlers/typebox/simple-type-handler.ts"></mcfile>, <mcfile name="literal-type-handler.ts" path="src/handlers/typebox/literal-type-handler.ts"></mcfile>
133-
6. **Advanced Handlers**: <mcfile name="template-literal-type-handler.ts" path="src/handlers/typebox/template-literal-type-handler.ts"></mcfile>, <mcfile name="type-operator-handler.ts" path="src/handlers/typebox/type-operator-handler.ts"></mcfile>, <mcfile name="keyof-type-handler.ts" path="src/handlers/typebox/keyof-type-handler.ts"></mcfile>, <mcfile name="readonly-type-handler.ts" path="src/handlers/typebox/readonly-type-handler.ts"></mcfile>
134-
7. **Function Handlers**: <mcfile name="function-type-handler.ts" path="src/handlers/typebox/function-type-handler.ts"></mcfile>
135-
8. **Type Query Handlers**: <mcfile name="type-query-handler.ts" path="src/handlers/typebox/type-query-handler.ts"></mcfile>, <mcfile name="typeof-type-handler.ts" path="src/handlers/typebox/typeof-type-handler.ts"></mcfile>
136-
9. **Access Handlers**: <mcfile name="indexed-access-type-handler.ts" path="src/handlers/typebox/indexed-access-type-handler.ts"></mcfile>, <mcfile name="type-reference-handler.ts" path="src/handlers/typebox/type-reference-handler.ts"></mcfile>
132+
5. **Readonly Handlers**: <mcfile name="readonly-type-handler.ts" path="src/handlers/typebox/readonly-type-handler.ts"></mcfile>, <mcfile name="readonly-array-type-handler.ts" path="src/handlers/typebox/readonly-array-type-handler.ts"></mcfile>
133+
6. **Simple Handlers**: <mcfile name="simple-type-handler.ts" path="src/handlers/typebox/simple-type-handler.ts"></mcfile>, <mcfile name="literal-type-handler.ts" path="src/handlers/typebox/literal-type-handler.ts"></mcfile>
134+
7. **Advanced Handlers**: <mcfile name="template-literal-type-handler.ts" path="src/handlers/typebox/template-literal-type-handler.ts"></mcfile>, <mcfile name="type-operator-handler.ts" path="src/handlers/typebox/type-operator-handler.ts"></mcfile>, <mcfile name="keyof-type-handler.ts" path="src/handlers/typebox/keyof-type-handler.ts"></mcfile>
135+
8. **Function Handlers**: <mcfile name="function-type-handler.ts" path="src/handlers/typebox/function-type-handler.ts"></mcfile>
136+
9. **Type Query Handlers**: <mcfile name="type-query-handler.ts" path="src/handlers/typebox/type-query-handler.ts"></mcfile>, <mcfile name="typeof-type-handler.ts" path="src/handlers/typebox/typeof-type-handler.ts"></mcfile>
137+
10. **Access Handlers**: <mcfile name="indexed-access-type-handler.ts" path="src/handlers/typebox/indexed-access-type-handler.ts"></mcfile>, <mcfile name="type-reference-handler.ts" path="src/handlers/typebox/type-reference-handler.ts"></mcfile>
138+
139+
#### Readonly Type Handling
140+
141+
The system provides comprehensive support for TypeScript's two distinct readonly constructs through a dual-handler approach:
142+
143+
1. **Readonly Utility Type**: `Readonly<T>` - Handled by <mcfile name="readonly-type-handler.ts" path="src/handlers/typebox/readonly-type-handler.ts"></mcfile>
144+
- Registered as a type reference handler for `Readonly` type references
145+
- Processes `TypeReferenceNode` with identifier "Readonly"
146+
- Generates `Type.Readonly(innerType)` for utility type syntax
147+
148+
2. **Readonly Array Modifier**: `readonly T[]` - Handled by <mcfile name="readonly-array-type-handler.ts" path="src/handlers/typebox/readonly-array-type-handler.ts"></mcfile>
149+
- Extends `TypeOperatorBaseHandler` for `ReadonlyKeyword` operator
150+
- Processes `TypeOperatorTypeNode` with `SyntaxKind.ReadonlyKeyword`
151+
- Generates `Type.Readonly(innerType)` for array modifier syntax
152+
- Registered as a fallback handler to handle complex readonly patterns
153+
154+
This dual approach ensures proper handling of both TypeScript readonly constructs:
155+
156+
- `type ReadonlyUser = Readonly<User>` (utility type)
157+
- `type ReadonlyArray = readonly string[]` (array modifier)
158+
- `type ReadonlyTuple = readonly [string, number]` (tuple modifier)
137159

138160
#### Handler Management
139161

140162
The <mcfile name="typebox-type-handlers.ts" path="src/handlers/typebox/typebox-type-handlers.ts"></mcfile> class orchestrates all handlers through:
141163

142164
- **Handler Caching**: Caches handler instances for performance optimization
143-
- **Fallback System**: Provides fallback handlers for complex cases
165+
- **Fallback System**: Provides fallback handlers for complex cases including readonly array modifiers
144166

145167
### Import Resolution
146168

@@ -181,6 +203,39 @@ export interface InputOptions {
181203
- **Source Code Input**: Processes TypeScript code directly from strings with validation
182204
- **Project Context**: Enables proper relative import resolution when working with in-memory source files
183205

206+
### Generic Type Support
207+
208+
The codebase provides comprehensive support for both generic interfaces and generic type aliases, enabling complex type transformations and reusable type definitions.
209+
210+
#### Generic Type Aliases
211+
212+
The `TypeAliasParser` handles both regular and generic type aliases through specialized processing:
213+
214+
1. **Type Parameter Detection**: Automatically detects type parameters using `typeAlias.getTypeParameters()`
215+
2. **Function Generation**: Creates TypeBox functions for generic type aliases with proper parameter constraints
216+
3. **TSchema Constraints**: Applies `TSchema` constraints to all type parameters for TypeBox compatibility
217+
4. **Static Type Generation**: Generates corresponding TypeScript type aliases using `Static<ReturnType<typeof TypeName<T>>>`
218+
219+
#### Generic Interface Support
220+
221+
Generic interfaces are processed through the `InterfaceParser` using a consistent architectural pattern that mirrors the type alias flow:
222+
223+
1. **Unified Generic Processing**: The interface parser now uses the same `GenericTypeUtils.createGenericArrowFunction` flow as type aliases for consistency
224+
2. **Raw Expression Handling**: The `InterfaceTypeHandler` returns raw TypeBox expressions for generic interfaces, allowing the parser to handle arrow function wrapping
225+
3. **Parameter Constraint Handling**: Converts TypeScript type parameter constraints to `TSchema` constraints using shared utilities
226+
4. **Function-Based Schema Generation**: Creates TypeBox schema functions that accept type parameters through the standardized generic arrow function pattern
227+
5. **Type Safety Preservation**: Maintains full TypeScript type safety through proper static type aliases using `Static<ReturnType<typeof TypeName<T>>>`
228+
6. **Architectural Consistency**: Both generic interfaces and type aliases now follow the same code generation pattern, improving maintainability and reducing duplication
229+
230+
#### Complex Generic Scenarios
231+
232+
The system supports advanced generic patterns including:
233+
234+
- **Multiple Type Parameters**: Functions with multiple generic parameters (e.g., `ApiResponse<T, E>`)
235+
- **Nested Generic Types**: Generic types that reference other generic types
236+
- **Utility Type Combinations**: Complex combinations like `Partial<Readonly<Record<K, V>>>`
237+
- **Type Parameter Propagation**: Proper handling of type parameters across nested type references
238+
184239
### Interface Inheritance
185240

186241
The codebase provides comprehensive support for TypeScript interface inheritance through a sophisticated dependency resolution and code generation system:
@@ -234,6 +289,16 @@ The <mcfile name="utils" path="src/utils"></mcfile> directory provides essential
234289
- **TypeBox Expression Generation**: Converts extracted keys into appropriate TypeBox array expressions
235290
- **Shared Utilities**: Provides reusable key extraction logic for Pick, Omit, and other utility type handlers to avoid code duplication
236291

292+
#### Generic Type Utilities
293+
294+
The <mcfile name="generic-type-utils.ts" path="src/utils/generic-type-utils.ts"></mcfile> module provides shared utilities for consistent generic type handling across parsers:
295+
296+
- **Generic Arrow Function Creation**: `createGenericArrowFunction` creates standardized arrow functions for generic types with proper type parameter constraints
297+
- **Type Parameter Processing**: Converts TypeScript type parameters to TypeBox-compatible function parameters with `TSchema` constraints
298+
- **Variable Statement Generation**: `addTypeBoxVariableStatement` creates consistent variable declarations for TypeBox schemas
299+
- **Generic Type Alias Generation**: `addGenericTypeAlias` creates standardized static type aliases using `Static<ReturnType<typeof TypeName<T>>>`
300+
- **Architectural Consistency**: Ensures both interface and type alias parsers follow the same generic type processing pattern
301+
237302
## Process Overview
238303

239304
1. **Input**: A TypeScript source file containing `enum`, `type alias`, `interface`, and `function` declarations.

src/handlers/typebox/object/interface-type-handler.ts

Lines changed: 18 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ObjectLikeBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/object/object-like-base-handler'
22
import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
3-
import { HeritageClause, InterfaceDeclaration, Node, ts, TypeParameterDeclaration } from 'ts-morph'
3+
import { HeritageClause, InterfaceDeclaration, Node, ts } from 'ts-morph'
44

55
export class InterfaceTypeHandler extends ObjectLikeBaseHandler {
66
canHandle(node: Node): boolean {
@@ -12,11 +12,26 @@ export class InterfaceTypeHandler extends ObjectLikeBaseHandler {
1212
const heritageClauses = node.getHeritageClauses()
1313
const baseObjectType = this.createObjectType(this.processProperties(node.getProperties()))
1414

15-
// If interface has type parameters, generate a function
15+
// For generic interfaces, return raw TypeBox expression
16+
// The parser will handle wrapping it in an arrow function using GenericTypeUtils
1617
if (typeParameters.length > 0) {
17-
return this.createGenericInterfaceFunction(typeParameters, baseObjectType, heritageClauses)
18+
// For generic interfaces, handle inheritance here and return raw expression
19+
if (heritageClauses.length === 0) {
20+
return baseObjectType
21+
}
22+
23+
const extendedTypes = this.collectExtendedTypes(heritageClauses)
24+
25+
if (extendedTypes.length === 0) {
26+
return baseObjectType
27+
}
28+
29+
// Create composite with extended types first, then the current interface
30+
const allTypes = [...extendedTypes, baseObjectType]
31+
return makeTypeCall('Composite', [ts.factory.createArrayLiteralExpression(allTypes, true)])
1832
}
1933

34+
// For non-generic interfaces, handle as before
2035
if (heritageClauses.length === 0) {
2136
return baseObjectType
2237
}
@@ -33,64 +48,6 @@ export class InterfaceTypeHandler extends ObjectLikeBaseHandler {
3348
return makeTypeCall('Composite', [ts.factory.createArrayLiteralExpression(allTypes, true)])
3449
}
3550

36-
private createGenericInterfaceFunction(
37-
typeParameters: TypeParameterDeclaration[],
38-
baseObjectType: ts.Expression,
39-
heritageClauses: HeritageClause[],
40-
): ts.Expression {
41-
// Create function parameters for each type parameter
42-
const functionParams = typeParameters.map((typeParam) => {
43-
const paramName = typeParam.getName()
44-
45-
return ts.factory.createParameterDeclaration(
46-
undefined,
47-
undefined,
48-
ts.factory.createIdentifier(paramName),
49-
undefined,
50-
ts.factory.createTypeReferenceNode(paramName, undefined),
51-
undefined,
52-
)
53-
})
54-
55-
// Create function body
56-
let functionBody: ts.Expression = baseObjectType
57-
58-
// Handle heritage clauses for generic interfaces
59-
const extendedTypes = this.collectExtendedTypes(heritageClauses)
60-
61-
if (extendedTypes.length > 0) {
62-
const allTypes = [...extendedTypes, baseObjectType]
63-
functionBody = makeTypeCall('Composite', [
64-
ts.factory.createArrayLiteralExpression(allTypes, true),
65-
])
66-
}
67-
68-
// Create type parameters for the function
69-
const functionTypeParams = typeParameters.map((typeParam) => {
70-
const paramName = typeParam.getName()
71-
72-
// Use TSchema as the constraint for TypeBox compatibility
73-
const constraintNode = ts.factory.createTypeReferenceNode('TSchema', undefined)
74-
75-
return ts.factory.createTypeParameterDeclaration(
76-
undefined,
77-
ts.factory.createIdentifier(paramName),
78-
constraintNode,
79-
undefined,
80-
)
81-
})
82-
83-
// Create arrow function
84-
return ts.factory.createArrowFunction(
85-
undefined,
86-
ts.factory.createNodeArray(functionTypeParams),
87-
functionParams,
88-
undefined,
89-
ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
90-
functionBody,
91-
)
92-
}
93-
9451
private parseGenericTypeCall(typeText: string): ts.Expression | null {
9552
const match = typeText.match(/^([^<]+)<([^>]+)>$/)
9653

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { TypeOperatorBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/type-operator-base-handler'
2+
import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
3+
import { SyntaxKind, ts } from 'ts-morph'
4+
5+
export class ReadonlyArrayTypeHandler extends TypeOperatorBaseHandler {
6+
protected readonly operatorKind = SyntaxKind.ReadonlyKeyword
7+
protected readonly typeBoxMethod = 'Readonly'
8+
9+
protected createTypeBoxCall(innerType: ts.Expression): ts.Expression {
10+
return makeTypeCall('Readonly', [innerType])
11+
}
12+
}
Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
import { TypeOperatorBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/type-operator-base-handler'
1+
import { TypeReferenceBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/reference/type-reference-base-handler'
2+
import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call'
23
import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
3-
import { SyntaxKind, ts } from 'ts-morph'
4+
import { Node, ts } from 'ts-morph'
45

5-
export class ReadonlyTypeHandler extends TypeOperatorBaseHandler {
6-
protected readonly operatorKind = SyntaxKind.ReadonlyKeyword
7-
protected readonly typeBoxMethod = 'Readonly'
6+
export class ReadonlyTypeHandler extends TypeReferenceBaseHandler {
7+
protected readonly supportedTypeNames = ['Readonly']
8+
protected readonly expectedArgumentCount = 1
89

9-
protected createTypeBoxCall(innerType: ts.Expression): ts.Expression {
10-
return makeTypeCall('Readonly', [innerType])
10+
handle(node: Node): ts.Expression {
11+
const typeRef = this.validateTypeReference(node)
12+
const [innerType] = this.extractTypeArguments(typeRef)
13+
14+
return makeTypeCall('Readonly', [getTypeBoxType(innerType)])
1115
}
1216
}

0 commit comments

Comments
 (0)