-
Notifications
You must be signed in to change notification settings - Fork 0
feat(interface-type-handler): Handle generic interfaces #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
This change adds support for handling generic interfaces in the `InterfaceTypeHandler`. Previously, the handler only supported non-generic interfaces. Now, if an interface has type parameters, the handler will generate a function that takes the type parameters as arguments and returns the composite type for the interface. The changes include: - Adding support for parsing type parameters and heritage clauses in the `handle` method. - Implementing the `createGenericInterfaceFunction` method to generate the function for generic interfaces. - Updating the `parseGenericTypeCall` method to handle generic type references in heritage clauses. These changes allow the `InterfaceTypeHandler` to correctly generate the TypeBox schema for generic interfaces, which is necessary for supporting more complex validation schemas.
📝 WalkthroughSummary by CodeRabbit
WalkthroughAdds support for generic TypeScript interfaces to the TypeBox code generator: handlers now emit parameterized factory functions for interfaces with type parameters, parsers generate corresponding generic type aliases (Static<ReturnType<...>>), import generation conditionally includes TSchema, tests and docs updated. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant TS as ts-morph-codegen
participant P as parse-interfaces.ts
participant H as interface-type-handler.ts
participant TB as TypeBox
Note over TS: Analyze source file for generic interfaces
TS->>TS: hasGenericInterfaces = any interface has type params?
TS->>TB: Import { Type, type Static, [type TSchema] }
loop For each InterfaceDeclaration
P->>P: Gather typeParameters, isExported
alt Generic interface
P->>H: parseGenericInterface(interfaceDecl, isExported)
H->>H: createGenericInterfaceFunction(params)
H->>H: parse extends (incl. GenericName<Arg>)
H->>TB: Build Composite([extends..., Type.Object({...})])
P->>P: addGenericTypeAlias(name, typeParameters, isExported)
else Non-generic interface
P->>H: parseRegularInterface(interfaceDecl, isExported)
H->>TB: Type.Object({...}) [+ Composite if extends]
P->>P: addStaticTypeAlias(name, isExported)
end
end
sequenceDiagram
autonumber
participant H as interface-type-handler.ts
participant TS as ts-morph (AST)
participant TB as TypeBox
H->>TS: node.getTypeParameters()
alt has type params
H->>H: createGenericInterfaceFunction
H->>TS: node.getHeritageClauses(Extends)
H->>H: parseGenericTypeCall for each type (Name<Arg> → Name(Type.X()))
H->>TB: Return Composite([extended..., Type.Object(...)])
else no type params
H->>TS: heritage extends?
H->>TB: Type.Object(...) or Composite([...])
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. 📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🧹 Nitpick comments (11)
ARCHITECTURE.md (2)
224-224: Docs: call out current generic-heritage limitations (single arg, primitives) or align with implementationThe text claims we “convert expressions like A to A(Type.Number())”. The current handler does this only for a single generic type argument and only for primitive args (number/string/boolean). Nested generics (e.g., A<B>), multiple args (A<K, V>), unions (A<string | number>), and qualified names (ns.A) are not handled by the current implementation and will generate invalid code paths. Please either (a) narrow the docs to reflect the implemented scope or (b) expand implementation per my inline suggestions in code.
260-261: Docs: “parse-interfaces.ts” note should clarify alias form for generics and import needsThe note about generating parameterized functions and generic type aliases is good. Please add one line clarifying that when a generic interface is local, we must import “type TSchema” and that the emitted alias uses Static<ReturnType<typeof Foo>>. Also call out the limitations mentioned above so expectations match the actual generator behavior.
src/handlers/typebox/object/interface-type-handler.ts (2)
52-122: Generic path duplicates heritage parsing logic; extract a shared helper to avoid driftThe generic path re-implements the same Extends parsing as the non-generic path. Please factor both call sites through a single helper that consumes HeritageClause[] and returns ts.Expression[] for extended types. This reduces divergence and centralizes bug fixes (see prior comment).
Here’s a minimal refactor to reuse the same logic:
@@ - // Handle heritage clauses for generic interfaces - if (heritageClauses.length > 0) { - const extendedTypes: ts.Expression[] = [] - for (const heritageClause of heritageClauses) { - if (heritageClause.getToken() !== ts.SyntaxKind.ExtendsKeyword) { - continue - } - for (const typeNode of heritageClause.getTypeNodes()) { - const typeText = typeNode.getText() - const genericCall = this.parseGenericTypeCall(typeText) - if (genericCall) { - extendedTypes.push(genericCall) - } else { - extendedTypes.push(ts.factory.createIdentifier(typeText)) - } - } - } - if (extendedTypes.length > 0) { - const allTypes = [...extendedTypes, baseObjectType] - functionBody = makeTypeCall('Composite', [ - ts.factory.createArrayLiteralExpression(allTypes, true), - ]) - } - } + // Handle heritage clauses (same logic as non-generic path) + const extended = this.collectExtendedTypes(heritageClauses) + if (extended.length > 0) { + const allTypes = [...extended, baseObjectType] + functionBody = makeTypeCall('Composite', [ + ts.factory.createArrayLiteralExpression(allTypes, true), + ]) + }And add:
+ private collectExtendedTypes(heritageClauses: HeritageClause[]): ts.Expression[] { + const out: ts.Expression[] = [] + for (const hc of heritageClauses) { + if (hc.getToken() !== ts.SyntaxKind.ExtendsKeyword) continue + for (const t of hc.getTypeNodes()) out.push(this.parseHeritageTypeNode(t)) + } + return out + }
102-111: Consider preserving original type parameter constraints along with TSchemaRight now every type parameter becomes
<T extends TSchema>, ignoring any original constraints (e.g.,<T extends string>). If keeping TSchema is required, we could intersect constraints when they’re simple (e.g., T extends TSchema & SomeConstraint) or gate this behind an option. If out of scope, at least document that original generic constraints are not preserved.Would you like me to prototype a constraint-preserving version that emits
T extends TSchemawhen unconstrained andT extends TSchema & (OriginalConstraint)when constrained?tests/handlers/typebox/interfaces.test.ts (1)
204-232: Great first coverage for generics; add a few edge cases to pin down behaviorConsider adding:
- Generic extends with type param passthrough:
interface B<T> extends A<T> { b: number }- Qualified base:
namespace NS { export interface A<T> { a: T } } interface B extends NS.A<number> { b: number }- Multiple type args:
interface A<K, V> { ... } interface B extends A<string, number> { ... }(document expected behavior if unsupported)- Nested generic arg:
interface B extends A<Array<number>> { ... }These will surface current gaps in the handler and prevent regressions once we switch to AST-driven parsing.
If you want, I can push a patch adding these tests and marking the unsupported ones with todo/skip until the handler is fixed.
tests/utils.ts (4)
6-9: Minor: simplify import builder APItypeboxImport currently accepts a string parameter used only for “, type TSchema”. A boolean is clearer.
Apply this tidy-up:
-const typeboxImport = (tschema: string = '') => - `import { Type, type Static${tschema} } from "@sinclair/typebox";\n` -const typeboxImportTSchema = () => typeboxImport(', type TSchema') +const typeboxImport = (withTSchema: boolean = false) => + `import { Type, type Static${withTSchema ? ', type TSchema' : ''} } from "@sinclair/typebox";\n` +const typeboxImportTSchema = () => typeboxImport(true)
14-21: formatWithPrettier: parameter namingThe parameter tschema is a boolean, but the name suggests a type. Consider renaming to withTSchema for clarity. No functional change.
-export const formatWithPrettier = ( - input: string, - addImport: boolean = true, - tschema: boolean = false, -): string => { - const importTypebox = tschema ? typeboxImportTSchema() : typeboxImport() +export const formatWithPrettier = ( + input: string, + addImport: boolean = true, + withTSchema: boolean = false, +): string => { + const importTypebox = withTSchema ? typeboxImportTSchema() : typeboxImport() const code = addImport ? `${importTypebox}${input}` : input
28-29: generateFormattedCode: mirror the naming change for consistencyFollow-up to the previous nit.
- tschema: boolean = false, + withTSchema: boolean = false,
37-38: Pass-through flag nameFollow-up to the previous nit.
- return formatWithPrettier(code, false, tschema) + return formatWithPrettier(code, false, withTSchema)src/parsers/parse-interfaces.ts (2)
74-100: Generic path is correct; minor duplication nitRecomputing
typeParametershere is harmless but unnecessary since it was already obtained in the caller. Consider threading it through to avoid duplicate work and slightly reduce surface area.If you want to trim the duplication:
- private parseGenericInterface(interfaceDecl: InterfaceDeclaration, isExported: boolean): void { - const interfaceName = interfaceDecl.getName() - const typeParameters = interfaceDecl.getTypeParameters() + private parseGenericInterface( + interfaceDecl: InterfaceDeclaration, + isExported: boolean, + typeParameters = interfaceDecl.getTypeParameters(), + ): void { + const interfaceName = interfaceDecl.getName()And at the call site:
- if (typeParameters.length > 0) { - this.parseGenericInterface(interfaceDecl, isExported) + if (typeParameters.length > 0) { + this.parseGenericInterface(interfaceDecl, isExported, typeParameters)
98-100: Optional: centralize alias emission to reduce duplicationConsider extending
addStaticTypeAliasto accept optionaltypeParametersand reuse it for both generic and non-generic cases. Keeps alias creation logic in one place.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (6)
ARCHITECTURE.md(2 hunks)src/handlers/typebox/object/interface-type-handler.ts(2 hunks)src/parsers/parse-interfaces.ts(3 hunks)src/ts-morph-codegen.ts(1 hunks)tests/handlers/typebox/interfaces.test.ts(1 hunks)tests/utils.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
tests/handlers/typebox/interfaces.test.ts (1)
tests/utils.ts (3)
createSourceFile(10-12)generateFormattedCode(25-38)formatWithPrettier(14-23)
tests/utils.ts (1)
src/ts-morph-codegen.ts (1)
generateCode(17-116)
src/handlers/typebox/object/interface-type-handler.ts (1)
src/utils/typebox-codegen-utils.ts (1)
makeTypeCall(3-12)
src/parsers/parse-interfaces.ts (1)
src/utils/typebox-call.ts (1)
getTypeBoxType(9-21)
🔇 Additional comments (7)
src/handlers/typebox/object/interface-type-handler.ts (2)
11-18: Good split between generic vs non-generic interfacesBranching early on type parameters keeps the handle path simple and avoids threading generics through the non-generic code path. This aligns with the tests.
49-50: Composite ordering is correctBase types first and then the current object is the expected order for Type.Composite([...bases, current]).
src/ts-morph-codegen.ts (2)
33-37: Generic import gating is correct but only checks local interfacesThe detection correctly enables “type TSchema” import when local source contains generic interfaces. If a local interface extends an imported generic, this remains fine because the callsite doesn’t need TSchema. Just calling it out for awareness.
39-53: Import structure: mixed named specifiers with type-only flags is idiomaticThe mix of value import (Type) and type-only imports (Static, TSchema) generates the desired
import { Type, type Static, type TSchema }form. Looks good.src/parsers/parse-interfaces.ts (3)
32-41: Solid generic vs. non-generic splitThe gating on
typeParameters.lengthis clear and keeps flows readable. No issues.
43-72: Non-generic handling looks goodEmitting a TypeBox const and delegating to
addStaticTypeAliaspreserves existing behavior cleanly.
124-133: [r�un_scripts]#!/bin/bash set -euo pipefail echo "TypeScript version (package.json):" jq -r '.devDependencies.typescript // .dependencies.typescript // .peerDependencies.typescript // "not found"' package.json echo "ts-morph version (package.json):" jq -r '.devDependencies["ts-morph"] // .dependencies["ts-morph"] // .peerDependencies["ts-morph"] // "not found"' package.json echo echo "TypeScript version from lockfile (package-lock.json):" grep -R '"typescript":' package-lock.json | head -n20 || true echo echo "ts-morph version from lockfile (package-lock.json):" grep -R '"ts-morph":' package-lock.json | head -n20 || true echo echo "Check Static and TSchema imports (double quotes):" rg 'import { Static, TSchema } from "@sinclair/typebox"' -g 'src/**/*.ts' -C2 || true echo echo "Check Static and TSchema imports (single quotes):" rg "import { Static, TSchema } from '@sinclair/typebox'" -g 'src/**/*.ts' -C2 || true
| for (const heritageClause of heritageClauses) { | ||
| if (heritageClause.getToken() === ts.SyntaxKind.ExtendsKeyword) { | ||
| for (const typeNode of heritageClause.getTypeNodes()) { | ||
| // For interface inheritance, we reference the already processed interface by name | ||
| const referencedTypeName = typeNode.getText() | ||
| extendedTypes.push(ts.factory.createIdentifier(referencedTypeName)) | ||
| if (heritageClause.getToken() !== ts.SyntaxKind.ExtendsKeyword) { | ||
| continue | ||
| } | ||
|
|
||
| for (const typeNode of heritageClause.getTypeNodes()) { | ||
| const typeText = typeNode.getText() | ||
| const genericCall = this.parseGenericTypeCall(typeText) | ||
| if (genericCall) { | ||
| extendedTypes.push(genericCall) | ||
| } else { | ||
| extendedTypes.push(ts.factory.createIdentifier(typeText)) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extends parsing via string getText() is fragile; qualified names and nested generics break generation
Using typeNode.getText() + regex to detect generics and falling back to ts.factory.createIdentifier(typeText) will produce invalid AST in several common cases:
- Qualified names: extends ns.A → Identifier("ns.A") is invalid; must be a property access expression.
- Nested generics: extends A<B> → parseGenericTypeCall fails and we fall back to Identifier("A<B>"), which is invalid.
- Multiple type args: extends A<K, V> → we pass a single argument "K, V" and then create Identifier("K, V") (invalid).
- Unions/arrays/complex args: A<string | number>, A<Record<string, number>> are not handled.
We need to construct these from the AST, not from text. See suggested refactor below; it removes regex parsing and builds the correct expressions for identifiers, property access (ns.A), and generic calls with N args.
Apply this diff to replace the fragile loop with AST-driven construction (uses ExpressionWithTypeArguments and supports multiple args and qualifiers):
@@
- for (const heritageClause of heritageClauses) {
- if (heritageClause.getToken() !== ts.SyntaxKind.ExtendsKeyword) {
- continue
- }
-
- for (const typeNode of heritageClause.getTypeNodes()) {
- const typeText = typeNode.getText()
- const genericCall = this.parseGenericTypeCall(typeText)
- if (genericCall) {
- extendedTypes.push(genericCall)
- } else {
- extendedTypes.push(ts.factory.createIdentifier(typeText))
- }
- }
- }
+ for (const heritageClause of heritageClauses) {
+ if (heritageClause.getToken() !== ts.SyntaxKind.ExtendsKeyword) continue
+ for (const typeNode of heritageClause.getTypeNodes()) {
+ extendedTypes.push(this.parseHeritageTypeNode(typeNode))
+ }
+ }And add the helpers below (see further diffs replacing parseGenericTypeCall/createTypeExpression).
📝 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.
| for (const heritageClause of heritageClauses) { | |
| if (heritageClause.getToken() === ts.SyntaxKind.ExtendsKeyword) { | |
| for (const typeNode of heritageClause.getTypeNodes()) { | |
| // For interface inheritance, we reference the already processed interface by name | |
| const referencedTypeName = typeNode.getText() | |
| extendedTypes.push(ts.factory.createIdentifier(referencedTypeName)) | |
| if (heritageClause.getToken() !== ts.SyntaxKind.ExtendsKeyword) { | |
| continue | |
| } | |
| for (const typeNode of heritageClause.getTypeNodes()) { | |
| const typeText = typeNode.getText() | |
| const genericCall = this.parseGenericTypeCall(typeText) | |
| if (genericCall) { | |
| extendedTypes.push(genericCall) | |
| } else { | |
| extendedTypes.push(ts.factory.createIdentifier(typeText)) | |
| } | |
| } | |
| for (const heritageClause of heritageClauses) { | |
| if (heritageClause.getToken() !== ts.SyntaxKind.ExtendsKeyword) continue | |
| for (const typeNode of heritageClause.getTypeNodes()) { | |
| extendedTypes.push(this.parseHeritageTypeNode(typeNode)) | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/handlers/typebox/object/interface-type-handler.ts around lines 26 to 39,
replace the fragile getText()/regex-based loop with AST-driven construction: for
each heritageClause.getTypeNodes(), inspect the node kind and build expressions
from the AST (handle Identifier/QualifiedName → Identifier or
PropertyAccessExpression, TypeReference with typeArguments → create a Generic
type expression using the node's typeName and recursively convert each
typeArgument into proper expressions, and handle nested generics/unions/arrays
by recursing into the type nodes), and push the resulting
Expression/ExpressionWithTypeArguments into extendedTypes; remove
parseGenericTypeCall/createTypeExpression usage and add helper(s) that convert
TypeNode/EntityName into the correct
ts.Expression/ts.PropertyAccessExpression/ts.CallExpression equivalents so
qualified names, multiple type args, and nested generics are produced from the
AST rather than from string parsing.
| private parseGenericTypeCall(typeText: string): ts.Expression | null { | ||
| const match = typeText.match(/^([^<]+)<([^>]+)>$/) | ||
|
|
||
| if (match && match[1] && match[2]) { | ||
| const baseName = match[1].trim() | ||
| const typeArg = match[2].trim() | ||
| return ts.factory.createCallExpression(ts.factory.createIdentifier(baseName), undefined, [ | ||
| this.createTypeExpression(typeArg), | ||
| ]) | ||
| } | ||
|
|
||
| return null | ||
| } | ||
|
|
||
| private createTypeExpression(typeArg: string): ts.Expression { | ||
| // Convert common TypeScript types to TypeBox calls | ||
| switch (typeArg) { | ||
| case 'number': | ||
| return makeTypeCall('Number') | ||
| case 'string': | ||
| return makeTypeCall('String') | ||
| case 'boolean': | ||
| return makeTypeCall('Boolean') | ||
| default: | ||
| // For other types, assume it's a reference | ||
| return ts.factory.createIdentifier(typeArg) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regex-based generic parsing produces invalid identifiers and misses common cases; build from AST
The current parseGenericTypeCall/createTypeExpression pair:
- Fails for nested generics and multiple args.
- Produces invalid Identifiers for qualified names and for “K, V”.
- Loses fidelity on non-primitive args.
Switch to AST-driven construction using ExpressionWithTypeArguments, proper property access for qualified names, and multiple type arguments. Also reuse the existing TypeBox type conversion for type arguments.
Apply this diff to replace both helpers with AST-based versions and support qualified names and N-ary generics. This uses ts-morph nodes and defers type-arg conversion to your existing typebox converter (getTypeBoxType). If getTypeBoxType is in a different module, update the import path accordingly.
@@
-import { HeritageClause, InterfaceDeclaration, Node, ts, TypeParameterDeclaration } from 'ts-morph'
+import {
+ HeritageClause,
+ InterfaceDeclaration,
+ Node,
+ ts,
+ TypeParameterDeclaration,
+ ExpressionWithTypeArguments,
+} from 'ts-morph'
+import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call'
@@
- private parseGenericTypeCall(typeText: string): ts.Expression | null {
- const match = typeText.match(/^([^<]+)<([^>]+)>$/)
-
- if (match && match[1] && match[2]) {
- const baseName = match[1].trim()
- const typeArg = match[2].trim()
- return ts.factory.createCallExpression(ts.factory.createIdentifier(baseName), undefined, [
- this.createTypeExpression(typeArg),
- ])
- }
-
- return null
- }
+ // Build expression for a heritage type: handles identifiers, ns.qualified identifiers,
+ // and generic calls with any number of type arguments.
+ private parseHeritageTypeNode(node: ExpressionWithTypeArguments): ts.Expression {
+ const exprText = node.getExpression().getText()
+ const baseExpr = this.createQualifiedAccess(exprText)
+ const typeArgs = node.getTypeArguments()
+ if (typeArgs.length === 0) return baseExpr
+ const argExprs = typeArgs.map((arg) => {
+ // Reuse the global TypeBox converter for full fidelity (unions, arrays, nested generics)
+ return getTypeBoxType(arg) as ts.Expression
+ })
+ return ts.factory.createCallExpression(baseExpr, undefined, argExprs)
+ }
@@
- private createTypeExpression(typeArg: string): ts.Expression {
- // Convert common TypeScript types to TypeBox calls
- switch (typeArg) {
- case 'number':
- return makeTypeCall('Number')
- case 'string':
- return makeTypeCall('String')
- case 'boolean':
- return makeTypeCall('Boolean')
- default:
- // For other types, assume it's a reference
- return ts.factory.createIdentifier(typeArg)
- }
- }
+ // ns.A.B -> PropertyAccessExpression chain; single segment -> Identifier
+ private createQualifiedAccess(name: string): ts.Expression {
+ const parts = name.split('.').map((p) => p.trim()).filter(Boolean)
+ if (parts.length === 0) return ts.factory.createIdentifier(name)
+ let acc: ts.Expression = ts.factory.createIdentifier(parts[0])
+ for (let i = 1; i < parts.length; i++) {
+ acc = ts.factory.createPropertyAccessExpression(acc, parts[i])
+ }
+ return acc
+ }If you prefer not to import getTypeBoxType here, we can inject a converter via the base handler or a utility to avoid cross-module dependencies. Let me know and I can adjust the patch.
| import { | ||
| InterfaceDeclaration, | ||
| ts, | ||
| TypeParameterDeclaration, | ||
| VariableDeclarationKind, | ||
| } from 'ts-morph' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Import structures needed for safe type-parameter emission
You’re constructing ts-morph structures later; pull in OptionalKind and TypeParameterDeclarationStructure so we can pass proper structures instead of pre-printed strings (see fix below).
import {
InterfaceDeclaration,
ts,
- TypeParameterDeclaration,
+ TypeParameterDeclaration,
+ OptionalKind,
+ TypeParameterDeclarationStructure,
VariableDeclarationKind,
} from 'ts-morph'📝 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.
| import { | |
| InterfaceDeclaration, | |
| ts, | |
| TypeParameterDeclaration, | |
| VariableDeclarationKind, | |
| } from 'ts-morph' | |
| // File: src/parsers/parse-interfaces.ts | |
| import { | |
| InterfaceDeclaration, | |
| ts, | |
| TypeParameterDeclaration, | |
| OptionalKind, | |
| TypeParameterDeclarationStructure, | |
| VariableDeclarationKind, | |
| } from 'ts-morph' |
🤖 Prompt for AI Agents
In src/parsers/parse-interfaces.ts around lines 4 to 9, the import list is
missing ts-morph helper types required for emitting safe type-parameter
structures; add OptionalKind and TypeParameterDeclarationStructure to the named
imports from 'ts-morph' so later code can pass
OptionalKind<TypeParameterDeclarationStructure> objects instead of pre-printed
strings; update any call sites to build and pass
TypeParameterDeclarationStructure objects (wrapped in OptionalKind) when
creating or setting type parameters.
| private addGenericTypeAlias( | ||
| name: string, | ||
| typeParameters: TypeParameterDeclaration[], | ||
| isExported: boolean, | ||
| ): void { | ||
| // Create type parameters for the type alias | ||
| const typeParamDeclarations = typeParameters.map((typeParam) => { | ||
| const paramName = typeParam.getName() | ||
| return ts.factory.createTypeParameterDeclaration( | ||
| undefined, | ||
| ts.factory.createIdentifier(paramName), | ||
| ts.factory.createTypeReferenceNode('TSchema', undefined), | ||
| undefined, | ||
| ) | ||
| }) | ||
|
|
||
| // Create the type: Static<ReturnType<typeof A<T>>> | ||
| const typeParamNames = typeParameters.map((tp) => tp.getName()) | ||
| const typeArguments = typeParamNames.map((paramName) => | ||
| ts.factory.createTypeReferenceNode(paramName, undefined), | ||
| ) | ||
|
|
||
| // Create typeof A<T> 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 = this.printer.printNode( | ||
| ts.EmitHint.Unspecified, | ||
| staticTypeNode, | ||
| this.newSourceFile.compilerNode, | ||
| ) | ||
|
|
||
| this.newSourceFile.addTypeAlias({ | ||
| isExported, | ||
| name, | ||
| typeParameters: typeParamDeclarations.map((tp) => | ||
| this.printer.printNode(ts.EmitHint.Unspecified, tp, this.newSourceFile.compilerNode), | ||
| ), | ||
| type: staticType, | ||
| }) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: addTypeAlias receives pre-printed type params; constraints are lost and may generate invalid code
You’re printing ts.TypeParameterDeclaration nodes to strings and passing them to addTypeAlias.typeParameters. ts-morph expects either parameter names or TypeParameterDeclarationStructure[]. Passing "T extends TSchema" as a raw string is treated as a name, not a structured param, which can yield invalid identifiers and drop the extends TSchema constraint.
Fix by passing proper structures and (optionally) typing them with OptionalKind<TypeParameterDeclarationStructure>; also reuse them directly in addTypeAlias.
private addGenericTypeAlias(
name: string,
typeParameters: TypeParameterDeclaration[],
isExported: boolean,
): void {
- // Create type parameters for the type alias
- const typeParamDeclarations = typeParameters.map((typeParam) => {
- const paramName = typeParam.getName()
- return ts.factory.createTypeParameterDeclaration(
- undefined,
- ts.factory.createIdentifier(paramName),
- ts.factory.createTypeReferenceNode('TSchema', undefined),
- undefined,
- )
- })
+ // Create type parameters for the type alias as ts-morph structures
+ const typeParamStructures: OptionalKind<TypeParameterDeclarationStructure>[] =
+ typeParameters.map((tp) => ({
+ name: tp.getName(),
+ constraint: 'TSchema',
+ }))
// Create the type: Static<ReturnType<typeof A<T>>>
const typeParamNames = typeParameters.map((tp) => tp.getName())
const typeArguments = typeParamNames.map((paramName) =>
ts.factory.createTypeReferenceNode(paramName, undefined),
)
// Create typeof A<T> 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 = this.printer.printNode(
ts.EmitHint.Unspecified,
staticTypeNode,
this.newSourceFile.compilerNode,
)
this.newSourceFile.addTypeAlias({
isExported,
name,
- typeParameters: typeParamDeclarations.map((tp) =>
- this.printer.printNode(ts.EmitHint.Unspecified, tp, this.newSourceFile.compilerNode),
- ),
+ typeParameters: typeParamStructures,
type: staticType,
})
}📝 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.
| private addGenericTypeAlias( | |
| name: string, | |
| typeParameters: TypeParameterDeclaration[], | |
| isExported: boolean, | |
| ): void { | |
| // Create type parameters for the type alias | |
| const typeParamDeclarations = typeParameters.map((typeParam) => { | |
| const paramName = typeParam.getName() | |
| return ts.factory.createTypeParameterDeclaration( | |
| undefined, | |
| ts.factory.createIdentifier(paramName), | |
| ts.factory.createTypeReferenceNode('TSchema', undefined), | |
| undefined, | |
| ) | |
| }) | |
| // Create the type: Static<ReturnType<typeof A<T>>> | |
| const typeParamNames = typeParameters.map((tp) => tp.getName()) | |
| const typeArguments = typeParamNames.map((paramName) => | |
| ts.factory.createTypeReferenceNode(paramName, undefined), | |
| ) | |
| // Create typeof A<T> 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 = this.printer.printNode( | |
| ts.EmitHint.Unspecified, | |
| staticTypeNode, | |
| this.newSourceFile.compilerNode, | |
| ) | |
| this.newSourceFile.addTypeAlias({ | |
| isExported, | |
| name, | |
| typeParameters: typeParamDeclarations.map((tp) => | |
| this.printer.printNode(ts.EmitHint.Unspecified, tp, this.newSourceFile.compilerNode), | |
| ), | |
| type: staticType, | |
| }) | |
| } | |
| private addGenericTypeAlias( | |
| name: string, | |
| typeParameters: TypeParameterDeclaration[], | |
| isExported: boolean, | |
| ): void { | |
| // Create type parameters for the type alias as ts-morph structures | |
| const typeParamStructures: OptionalKind<TypeParameterDeclarationStructure>[] = | |
| typeParameters.map((tp) => ({ | |
| name: tp.getName(), | |
| constraint: 'TSchema', | |
| })) | |
| // Create the type: Static<ReturnType<typeof A<T>>> | |
| const typeParamNames = typeParameters.map((tp) => tp.getName()) | |
| const typeArguments = typeParamNames.map((paramName) => | |
| ts.factory.createTypeReferenceNode(paramName, undefined), | |
| ) | |
| // Create typeof A<T> 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 = this.printer.printNode( | |
| ts.EmitHint.Unspecified, | |
| staticTypeNode, | |
| this.newSourceFile.compilerNode, | |
| ) | |
| this.newSourceFile.addTypeAlias({ | |
| isExported, | |
| name, | |
| typeParameters: typeParamStructures, | |
| type: staticType, | |
| }) | |
| } |
This change adds support for handling generic interfaces in the
InterfaceTypeHandler. Previously, the handler only supportednon-generic interfaces. Now, if an interface has type parameters,
the handler will generate a function that takes the type parameters
as arguments and returns the composite type for the interface.
The changes include:
in the
handlemethod.createGenericInterfaceFunctionmethod togenerate the function for generic interfaces.
parseGenericTypeCallmethod to handle generictype references in heritage clauses.
These changes allow the
InterfaceTypeHandlerto correctlygenerate the TypeBox schema for generic interfaces, which is
necessary for supporting more complex validation schemas.