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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ const result = await generateCode({
- <mcfile name="typebox-call.ts" path="src/utils/typebox-call.ts"></mcfile>: Contains the core logic for converting TypeScript type nodes into TypeBox `Type` expressions. `getTypeBoxType` takes a `TypeNode` as input and returns a `ts.Node` representing the equivalent TypeBox schema.
- <mcfile name="add-static-type-alias.ts" path="src/utils/add-static-type-alias.ts"></mcfile>: Generates and adds the `export type [TypeName] = Static<typeof [TypeName]>` declaration to the output source file. This declaration is essential for enabling TypeScript's static type inference from the dynamically generated TypeBox schemas, ensuring type safety at compile time.
- <mcfile name="typebox-codegen-utils.ts" path="src/utils/typebox-codegen-utils.ts"></mcfile>: Contains general utility functions that support the TypeBox code generation process, such as helper functions for string manipulation or AST node creation.
- <mcfile name="key-extraction-utils.ts" path="src/utils/key-extraction-utils.ts"></mcfile>: Provides shared utilities for extracting string literal keys from union or literal types, used by Pick and Omit type handlers to avoid code duplication.
- <mcfile name="node-type-utils.ts" path="src/utils/node-type-utils.ts"></mcfile>: Contains common Node type checking utilities for `canHandle` methods, including functions for checking SyntaxKind, TypeOperator patterns, and TypeReference patterns.

### Handlers Directory

Expand All @@ -207,6 +209,7 @@ This directory contains a collection of specialized handler modules, each respon
- <mcfile name="type-reference-base-handler.ts" path="src/handlers/typebox/reference/type-reference-base-handler.ts"></mcfile>: Specialized base class for utility type handlers that work with TypeScript type references. Provides `validateTypeReference` and `extractTypeArguments` methods for consistent handling of generic utility types like `Partial<T>`, `Pick<T, K>`, etc.
- <mcfile name="object-like-base-handler.ts" path="src/handlers/typebox/object/object-like-base-handler.ts"></mcfile>: Base class for handlers that process object-like structures (objects and interfaces). Provides `processProperties`, `extractProperties`, and `createObjectType` methods for consistent property handling and TypeBox object creation.
- <mcfile name="collection-base-handler.ts" path="src/handlers/typebox/collection/collection-base-handler.ts"></mcfile>: Base class for handlers that work with collections of types (arrays, tuples, unions, intersections). Provides `processTypeCollection`, `processSingleType`, and `validateNonEmptyCollection` methods for consistent type collection processing.
- <mcfile name="type-operator-base-handler.ts" path="src/handlers/typebox/type-operator-base-handler.ts"></mcfile>: Base class for TypeScript type operator handlers (keyof, readonly). Provides common functionality for checking operator types using `isTypeOperatorWithOperator` utility and processing inner types. Subclasses define `operatorKind`, `typeBoxMethod`, and `createTypeBoxCall` to customize behavior for specific operators.

#### Type Handler Implementations

Expand All @@ -230,15 +233,18 @@ This directory contains a collection of specialized handler modules, each respon
- <mcfile name="union-type-handler.ts" path="src/handlers/typebox/collection/union-type-handler.ts"></mcfile>: Handles TypeScript union types (e.g., `string | number`).
- <mcfile name="intersection-type-handler.ts" path="src/handlers/typebox/collection/intersection-type-handler.ts"></mcfile>: Handles TypeScript intersection types (e.g., `TypeA & TypeB`).

**Type Operator Handlers** (extend `TypeOperatorBaseHandler`):

- <mcfile name="keyof-type-handler.ts" path="src/handlers/typebox/keyof-type-handler.ts"></mcfile>: Handles TypeScript `keyof` type operator for extracting object keys.
- <mcfile name="readonly-type-handler.ts" path="src/handlers/typebox/readonly-type-handler.ts"></mcfile>: Handles TypeScript `readonly` type modifier for creating immutable types.

**Standalone Type Handlers** (extend `BaseTypeHandler`):

- <mcfile name="simple-type-handler.ts" path="src/handlers/typebox/simple-type-handler.ts"></mcfile>: Handles basic TypeScript types like `string`, `number`, `boolean`, `null`, `undefined`, `any`, `unknown`, `void`.
- <mcfile name="literal-type-handler.ts" path="src/handlers/typebox/literal-type-handler.ts"></mcfile>: Handles TypeScript literal types (e.g., `'hello'`, `123`, `true`).
- <mcfile name="function-type-handler.ts" path="src/handlers/typebox/function-type-handler.ts"></mcfile>: Handles TypeScript function types and function declarations, including parameter types, optional parameters, and return types.
- <mcfile name="template-literal-type-handler.ts" path="src/handlers/typebox/template-literal-type-handler.ts"></mcfile>: Handles TypeScript template literal types (e.g., `` `hello-${string}` ``). Parses template literals into components, handling literal text, embedded types (string, number, unions), and string/numeric literals.
- <mcfile name="typeof-type-handler.ts" path="src/handlers/typebox/typeof-type-handler.ts"></mcfile>: Handles TypeScript `typeof` expressions for extracting types from values.
- <mcfile name="keyof-type-handler.ts" path="src/handlers/typebox/keyof-type-handler.ts"></mcfile>: Handles TypeScript `keyof` type operator for extracting object keys.
- <mcfile name="readonly-type-handler.ts" path="src/handlers/typebox/readonly-type-handler.ts"></mcfile>: Handles TypeScript `readonly` type modifier for creating immutable types.
- <mcfile name="type-operator-handler.ts" path="src/handlers/typebox/type-operator-handler.ts"></mcfile>: Fallback handler for other TypeScript type operators not covered by specific handlers.
- <mcfile name="type-reference-handler.ts" path="src/handlers/typebox/type-reference-handler.ts"></mcfile>: Handles references to other types (e.g., `MyType`).
- <mcfile name="indexed-access-type-handler.ts" path="src/handlers/typebox/indexed-access-type-handler.ts"></mcfile>: Handles TypeScript indexed access types (e.g., `Type[Key]`).
Expand Down
19 changes: 7 additions & 12 deletions src/handlers/typebox/keyof-type-handler.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler'
import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call'
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 { Node, SyntaxKind, ts, TypeOperatorTypeNode } from 'ts-morph'
import { SyntaxKind, ts } from 'ts-morph'

export class KeyOfTypeHandler extends BaseTypeHandler {
canHandle(node: Node): boolean {
return Node.isTypeOperatorTypeNode(node) && node.getOperator() === SyntaxKind.KeyOfKeyword
}

handle(node: TypeOperatorTypeNode): ts.Expression {
const operandType = node.getTypeNode()
const typeboxOperand = getTypeBoxType(operandType)
export class KeyOfTypeHandler extends TypeOperatorBaseHandler {
protected readonly operatorKind = SyntaxKind.KeyOfKeyword
protected readonly typeBoxMethod = 'KeyOf'

return makeTypeCall('KeyOf', [typeboxOperand])
protected createTypeBoxCall(innerType: ts.Expression): ts.Expression {
return makeTypeCall('KeyOf', [innerType])
}
}
20 changes: 7 additions & 13 deletions src/handlers/typebox/readonly-type-handler.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler'
import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call'
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 { Node, SyntaxKind, ts, TypeOperatorTypeNode } from 'ts-morph'
import { SyntaxKind, ts } from 'ts-morph'

export class ReadonlyTypeHandler extends BaseTypeHandler {
canHandle(node: Node): boolean {
return Node.isTypeOperatorTypeNode(node) && node.getOperator() === SyntaxKind.ReadonlyKeyword
}

handle(node: TypeOperatorTypeNode): ts.Expression {
const operandType = node.getTypeNode()
const typeboxOperand = getTypeBoxType(operandType)
export class ReadonlyTypeHandler extends TypeOperatorBaseHandler {
protected readonly operatorKind = SyntaxKind.ReadonlyKeyword
protected readonly typeBoxMethod = 'Readonly'

// TypeBox uses Readonly utility type for readonly modifiers
return makeTypeCall('Readonly', [typeboxOperand])
protected createTypeBoxCall(innerType: ts.Expression): ts.Expression {
return makeTypeCall('Readonly', [innerType])
}
}
36 changes: 6 additions & 30 deletions src/handlers/typebox/reference/omit-type-handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { TypeReferenceBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/reference/type-reference-base-handler'
import {
createTypeBoxKeys,
extractStringKeys,
} from '@daxserver/validation-schema-codegen/utils/key-extraction-utils'
import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call'
import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
import { Node, ts } from 'ts-morph'
Expand All @@ -16,36 +20,8 @@ export class OmitTypeHandler extends TypeReferenceBaseHandler {
}

const typeboxObjectType = getTypeBoxType(objectType)

let omitKeys: string[] = []
if (Node.isUnionTypeNode(keysType)) {
omitKeys = keysType.getTypeNodes().map((unionType) => {
if (Node.isLiteralTypeNode(unionType)) {
const literalExpression = unionType.getLiteral()
if (Node.isStringLiteral(literalExpression)) {
return literalExpression.getLiteralText()
}
}
return '' // Should not happen if keys are string literals
})
} else if (Node.isLiteralTypeNode(keysType)) {
const literalExpression = keysType.getLiteral()
if (Node.isStringLiteral(literalExpression)) {
omitKeys = [literalExpression.getLiteralText()]
}
}

let typeboxKeys: ts.Expression
if (omitKeys.length === 1) {
typeboxKeys = makeTypeCall('Literal', [ts.factory.createStringLiteral(omitKeys[0]!)])
} else {
typeboxKeys = makeTypeCall('Union', [
ts.factory.createArrayLiteralExpression(
omitKeys.map((k) => makeTypeCall('Literal', [ts.factory.createStringLiteral(k)])),
true,
),
])
}
const omitKeys = extractStringKeys(keysType)
const typeboxKeys = createTypeBoxKeys(omitKeys)

Comment on lines 22 to 25
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Fix empty/partial key extraction: avoid Type.Union([]) and lossy omission.

If keysType isn’t a pure union of string literals (e.g., keyof T, or a mixed union), extractStringKeys returns [] or a partial list. Passing that to createTypeBoxKeys builds an invalid union and/or drops members, changing semantics.

Apply a safe fallback to getTypeBoxType(keysType) unless the union is fully string-literal:

-    const typeboxObjectType = getTypeBoxType(objectType)
-    const omitKeys = extractStringKeys(keysType)
-    const typeboxKeys = createTypeBoxKeys(omitKeys)
+    const typeboxObjectType = getTypeBoxType(objectType)
+    const omitKeys = extractStringKeys(keysType)
+    const isAllStringLiteralUnion =
+      (Node.isLiteralTypeNode(keysType) && Node.isStringLiteral(keysType.getLiteral())) ||
+      (Node.isUnionTypeNode(keysType) &&
+        keysType.getTypeNodes().every(
+          (t) => Node.isLiteralTypeNode(t) && Node.isStringLiteral(t.getLiteral()),
+        ))
+    const typeboxKeys =
+      omitKeys.length > 0 && isAllStringLiteralUnion
+        ? createTypeBoxKeys(omitKeys)
+        : getTypeBoxType(keysType)

Please mirror the same logic in PickTypeHandler for consistency. I can submit the corresponding change if helpful.


🏁 Script executed:

#!/bin/bash
# Search for extractStringKeys definition and usage
rg -n "extractStringKeys" -C3 --glob '*.ts'
# Search for createTypeBoxKeys definition and usage
rg -n "createTypeBoxKeys" -C3 --glob '*.ts'
# Search for getTypeBoxType definition and usage
rg -n "getTypeBoxType" -C3 --glob '*.ts'
# List TypeBox handler files for context
fd --extension ts src/handlers/typebox/reference

Length of output: 27174


🏁 Script executed:

#!/bin/bash
# Display extractStringKeys and createTypeBoxKeys definitions for verification
sed -n '1,200p' src/utils/key-extraction-utils.ts

Length of output: 1546


🏁 Script executed:

#!/bin/bash
sed -n '1,200p' src/handlers/typebox/reference/pick-type-handler.ts

Length of output: 1147


🏁 Script executed:

#!/bin/bash
# Dump first 200 lines of omit-type-handler.ts for review
sed -n '1,200p' src/handlers/typebox/reference/omit-type-handler.ts

Length of output: 1147


Prevent invalid key unions in Omit & Pick handlers

Both OmitTypeHandler and PickTypeHandler always call

const keys = extractStringKeys(keysType)
const typeboxKeys = createTypeBoxKeys(keys)

but extractStringKeys only returns the full set of string literals when keysType is literally a union of string literals or a single literal. For any other expression—keyof T, T extends U ? …, mixed unions—extractStringKeys returns [] (or a subset), leading to Type.Union([]) or dropped members and a semantic mismatch.

You should guard against this by falling back to the raw keysType via getTypeBoxType(keysType) unless:

  • keysType is a literal or a union of only string-literal type nodes, and
  • extractStringKeys returned at least one key.

Please apply the same fix in both handlers:

src/handlers/typebox/reference/omit-type-handler.ts (in handle):

-    const typeboxObjectType = getTypeBoxType(objectType)
-    const omitKeys = extractStringKeys(keysType)
-    const typeboxKeys = createTypeBoxKeys(omitKeys)
+    const typeboxObjectType = getTypeBoxType(objectType)
+    const omitKeys = extractStringKeys(keysType)
+    const isAllStringLiterals =
+      (Node.isLiteralTypeNode(keysType) && Node.isStringLiteral(keysType.getLiteral())) ||
+      (Node.isUnionTypeNode(keysType) &&
+        keysType.getTypeNodes().every(
+          (t) => Node.isLiteralTypeNode(t) && Node.isStringLiteral(t.getLiteral()),
+        ))
+    const typeboxKeys =
+      omitKeys.length > 0 && isAllStringLiterals
+        ? createTypeBoxKeys(omitKeys)
+        : getTypeBoxType(keysType)

src/handlers/typebox/reference/pick-type-handler.ts (in handle):

-    const typeboxObjectType = getTypeBoxType(objectType)
-    const pickKeys = extractStringKeys(keysType)
-    const typeboxKeys = createTypeBoxKeys(pickKeys)
+    const typeboxObjectType = getTypeBoxType(objectType)
+    const pickKeys = extractStringKeys(keysType)
+    const isAllStringLiterals =
+      (Node.isLiteralTypeNode(keysType) && Node.isStringLiteral(keysType.getLiteral())) ||
+      (Node.isUnionTypeNode(keysType) &&
+        keysType.getTypeNodes().every(
+          (t) => Node.isLiteralTypeNode(t) && Node.isStringLiteral(t.getLiteral()),
+        ))
+    const typeboxKeys =
+      pickKeys.length > 0 && isAllStringLiterals
+        ? createTypeBoxKeys(pickKeys)
+        : getTypeBoxType(keysType)

This ensures that whenever we can’t reliably extract every literal key, we let TypeBox handle the original keysType expression instead of generating an empty or lossy union.

🤖 Prompt for AI Agents
In src/handlers/typebox/reference/omit-type-handler.ts around lines 22-25, and
likewise in src/handlers/typebox/reference/pick-type-handler.ts in the
equivalent spot, currently you call extractStringKeys(keysType) and then
unconditionally createTypeBoxKeys(omitKeys). Change this to: call
extractStringKeys(keysType) as before, but only call createTypeBoxKeys(omitKeys)
when keysType is a string-literal or a union of string-literals AND
extractStringKeys returned a non-empty array; otherwise call
getTypeBoxType(keysType) and use that as the TypeBox representation of the keys.
Implement the literal-or-union check (e.g. helper
isStringLiteralOrUnion(keysType) or inline AST checks) and use it to choose
between createTypeBoxKeys(omitKeys) and getTypeBoxType(keysType) so we fall back
to the original keysType when extraction is unreliable.

return makeTypeCall('Omit', [typeboxObjectType, typeboxKeys])
}
Expand Down
36 changes: 6 additions & 30 deletions src/handlers/typebox/reference/pick-type-handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { TypeReferenceBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/reference/type-reference-base-handler'
import {
createTypeBoxKeys,
extractStringKeys,
} from '@daxserver/validation-schema-codegen/utils/key-extraction-utils'
import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call'
import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
import { Node, ts } from 'ts-morph'
Expand All @@ -16,36 +20,8 @@ export class PickTypeHandler extends TypeReferenceBaseHandler {
}

const typeboxObjectType = getTypeBoxType(objectType)

let pickKeys: string[] = []
if (Node.isUnionTypeNode(keysType)) {
pickKeys = keysType.getTypeNodes().map((unionType) => {
if (Node.isLiteralTypeNode(unionType)) {
const literalExpression = unionType.getLiteral()
if (Node.isStringLiteral(literalExpression)) {
return literalExpression.getLiteralText()
}
}
return '' // Should not happen if keys are string literals
})
} else if (Node.isLiteralTypeNode(keysType)) {
const literalExpression = keysType.getLiteral()
if (Node.isStringLiteral(literalExpression)) {
pickKeys = [literalExpression.getLiteralText()]
}
}

let typeboxKeys: ts.Expression
if (pickKeys.length === 1) {
typeboxKeys = makeTypeCall('Literal', [ts.factory.createStringLiteral(pickKeys[0]!)])
} else {
typeboxKeys = makeTypeCall('Union', [
ts.factory.createArrayLiteralExpression(
pickKeys.map((k) => makeTypeCall('Literal', [ts.factory.createStringLiteral(k)])),
false,
),
])
}
const pickKeys = extractStringKeys(keysType)
const typeboxKeys = createTypeBoxKeys(pickKeys)

Comment on lines +23 to 25
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

Bug risk: empty/unsupported keys produce Type.Union([]); add a safe fallback and dedupe.

When keysType is not a union of string literal types (e.g., Pick<T, keyof T>, numeric/symbol keys, aliased unions), extractStringKeys returns [], and createTypeBoxKeys([]) builds Type.Union([]). That’s likely invalid for Type.Pick and deviates from TS semantics (Pick<T, never> -> {}), causing incorrect codegen. Use getTypeBoxType(keysType) as a fallback, and dedupe keys to avoid repeated literals.

Apply this diff:

-    const pickKeys = extractStringKeys(keysType)
-    const typeboxKeys = createTypeBoxKeys(pickKeys)
+    const pickKeys = extractStringKeys(keysType)
+    const typeboxKeys =
+      pickKeys.length > 0
+        ? createTypeBoxKeys([...new Set(pickKeys)])
+        : getTypeBoxType(keysType)

Follow-up:

  • Consider explicitly handling the “no keys” case by emitting a keys expression that results in an empty object schema (mirroring Pick<T, never>). If TypeBox lacks a canonical representation for “never keys,” the above fallback is still safer than emitting an empty union.

Run quick checks to find potentially affected call sites:


🏁 Script executed:

#!/usr/bin/env bash
# Pick/Omit with `keyof` (unsupported by extractStringKeys) that may regress.
rg -nP --type=ts -C2 '\bPick<[^,]+,\s*keyof\b'
rg -nP --type=ts -C2 '\bOmit<[^,]+,\s*keyof\b'
# Picks with non-string literal unions
rg -nP --type=ts -C2 '\bPick<[^,]+,\s*[^\'"]' | rg -nP -v "['\"]"

Length of output: 214


🏁 Script executed:

#!/usr/bin/env bash
# Quick check: Pick/Omit with `keyof` (unsupported by extractStringKeys)
rg -nP --type=ts -C2 "\\bPick<[^,]+,\\s*keyof\\b"
rg -nP --type=ts -C2 "\\bOmit<[^,]+,\\s*keyof\\b"

# Note: picks with non-string-literal unions are more complex to detect reliably via regex;
# please review manually for cases like Pick<T, MyUnionOfKeys> where MyUnionOfKeys isn’t a string literal union.

Length of output: 97


Enforce safe fallback and dedupe for Pick-type key handling

A search for direct Pick<…, keyof …> and Omit<…, keyof …> patterns in the codebase returned no hits, so there are no immediate regressions. Nevertheless, to prevent emitting Type.Union([]) when extractStringKeys yields an empty array—and to mirror the TypeScript semantics of Pick<T, never> → {}—please apply this change in src/handlers/typebox/reference/pick-type-handler.ts (around lines 23–25):

-    const pickKeys = extractStringKeys(keysType)
-    const typeboxKeys = createTypeBoxKeys(pickKeys)
+    const pickKeys = extractStringKeys(keysType)
+    const typeboxKeys =
+      pickKeys.length > 0
+        ? createTypeBoxKeys([...new Set(pickKeys)])
+        : getTypeBoxType(keysType)

• Dedupe literal keys via new Set(pickKeys).
• Fallback to getTypeBoxType(keysType) to safely handle empty or unsupported unions.

📝 Committable suggestion

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

Suggested change
const pickKeys = extractStringKeys(keysType)
const typeboxKeys = createTypeBoxKeys(pickKeys)
const pickKeys = extractStringKeys(keysType)
const typeboxKeys =
pickKeys.length > 0
? createTypeBoxKeys([...new Set(pickKeys)])
: getTypeBoxType(keysType)
🤖 Prompt for AI Agents
In src/handlers/typebox/reference/pick-type-handler.ts around lines 23 to 25,
the current logic calls extractStringKeys(keysType) then
createTypeBoxKeys(pickKeys) which can produce Type.Union([]) when pickKeys is
empty; dedupe the keys by using new Set(pickKeys) before passing to
createTypeBoxKeys, and if the resulting array is empty, return
getTypeBoxType(keysType) as a safe fallback (this prevents emitting an empty
union and mirrors TypeScript's Pick<T, never> → {}). Ensure the code transforms
the Set back to an array for createTypeBoxKeys and only calls createTypeBoxKeys
when that array is non-empty; otherwise call and return
getTypeBoxType(keysType).

return makeTypeCall('Pick', [typeboxObjectType, typeboxKeys])
}
Expand Down
27 changes: 15 additions & 12 deletions src/handlers/typebox/simple-type-handler.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler'
import { isAnySyntaxKind } from '@daxserver/validation-schema-codegen/utils/node-type-utils'
import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
import { Node, SyntaxKind, ts } from 'ts-morph'

export const TypeBoxType = 'Type'

type SimpleKinds =
| SyntaxKind.AnyKeyword
| SyntaxKind.BooleanKeyword
| SyntaxKind.NeverKeyword
| SyntaxKind.NullKeyword
| SyntaxKind.NumberKeyword
| SyntaxKind.StringKeyword
| SyntaxKind.UnknownKeyword
| SyntaxKind.VoidKeyword
const SimpleKinds = [
SyntaxKind.AnyKeyword,
SyntaxKind.BooleanKeyword,
SyntaxKind.NeverKeyword,
SyntaxKind.NullKeyword,
SyntaxKind.NumberKeyword,
SyntaxKind.StringKeyword,
SyntaxKind.UnknownKeyword,
SyntaxKind.VoidKeyword,
] as const
type SimpleKind = (typeof SimpleKinds)[number]

const kindToTypeBox: Record<SimpleKinds, string> = {
const kindToTypeBox: Record<SimpleKind, string> = {
[SyntaxKind.AnyKeyword]: 'Any',
[SyntaxKind.BooleanKeyword]: 'Boolean',
[SyntaxKind.NeverKeyword]: 'Never',
Expand All @@ -27,10 +30,10 @@ const kindToTypeBox: Record<SimpleKinds, string> = {

export class SimpleTypeHandler extends BaseTypeHandler {
canHandle(node: Node): boolean {
return node.getKind() in kindToTypeBox
return isAnySyntaxKind(node, SimpleKinds)
}

handle(node: Node): ts.Expression {
return makeTypeCall(kindToTypeBox[node.getKind() as SimpleKinds])
return makeTypeCall(kindToTypeBox[node.getKind() as SimpleKind])
}
}
25 changes: 25 additions & 0 deletions src/handlers/typebox/type-operator-base-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler'
import { isTypeOperatorWithOperator } from '@daxserver/validation-schema-codegen/utils/node-type-utils'
import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call'
import { Node, SyntaxKind, ts, TypeOperatorTypeNode } from 'ts-morph'

/**
* Base class for TypeOperator handlers (KeyOf, Readonly, etc.)
* Provides common functionality for handling TypeOperatorTypeNode
*/
export abstract class TypeOperatorBaseHandler extends BaseTypeHandler {
protected abstract readonly operatorKind: SyntaxKind
protected abstract readonly typeBoxMethod: string

canHandle(node: Node): boolean {
return isTypeOperatorWithOperator(node, this.operatorKind)
}

handle(node: TypeOperatorTypeNode): ts.Expression {
const innerType = node.getTypeNode()
const typeboxInnerType = getTypeBoxType(innerType)
return this.createTypeBoxCall(typeboxInnerType)
}
Comment on lines +18 to +22
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Confirm override compatibility of handle(...) with BaseTypeHandler.

This class overrides handle(node: TypeOperatorTypeNode), which is narrower than a common BaseTypeHandler signature of handle(node: Node). If BaseTypeHandler indeed declares handle(node: Node), this override will not be type-compatible in TS. If so, change the signature to Node and narrow inside.

Run:

If BaseTypeHandler expects Node, consider:

-  handle(node: TypeOperatorTypeNode): ts.Expression {
-    const innerType = node.getTypeNode()
+  handle(node: Node): ts.Expression {
+    // canHandle() guarantees TypeOperatorTypeNode; assert to satisfy typing
+    const opNode = node as TypeOperatorTypeNode
+    const innerType = opNode.getTypeNode()
     const typeboxInnerType = getTypeBoxType(innerType)
     return this.createTypeBoxCall(typeboxInnerType)
   }

🏁 Script executed:

#!/usr/bin/env bash
# Inspect BaseTypeHandler.handle signature and all overrides.
rg -nP --type=ts -C3 'class\s+BaseTypeHandler\b'
rg -nP --type=ts -C2 'abstract\s+handle\s*\(\s*node\s*:\s*Node'
rg -nP --type=ts -C2 'handle\s*\(\s*node\s*:\s*TypeOperatorTypeNode'

Length of output: 1772


Ensure handle signature matches BaseTypeHandler

The base class declares

abstract handle(node: Node): ts.Expression

but here you’ve overridden it with a narrower parameter type (TypeOperatorTypeNode), which breaks compatibility (and will error if you use the override keyword or enable stricter checks). Please update the signature to accept Node and then narrow internally:

-  handle(node: TypeOperatorTypeNode): ts.Expression {
-    const innerType = node.getTypeNode()
+  handle(node: Node): ts.Expression {
+    // `canHandle` already ensures this is a TypeOperatorTypeNode
+    const opNode = node as TypeOperatorTypeNode
+    const innerType = opNode.getTypeNode()
     const typeboxInnerType = getTypeBoxType(innerType)
     return this.createTypeBoxCall(typeboxInnerType)
   }

• File needing update:
src/handlers/typebox/type-operator-base-handler.ts (around line 18)

🤖 Prompt for AI Agents
In src/handlers/typebox/type-operator-base-handler.ts around lines 18 to 22, the
handle method signature currently uses a narrower TypeOperatorTypeNode which
mismatches BaseTypeHandler's abstract handle(node: Node): ts.Expression; change
the method signature to handle(node: Node): ts.Expression and inside the method
perform a runtime narrowing (e.g. assert/if check or cast) to obtain a
TypeOperatorTypeNode before using getTypeNode/getTypeBoxType/createTypeBoxCall
so the override is compatible with the base class and strict checks.


protected abstract createTypeBoxCall(innerType: ts.Expression): ts.Expression
}
45 changes: 45 additions & 0 deletions src/utils/key-extraction-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
import { Node, ts } from 'ts-morph'

/**
* Extracts string keys from a TypeScript type node (typically used for Pick/Omit key types)
* Handles both single literal types and union types containing string literals
*/
export const extractStringKeys = (keysType: Node): string[] => {
const keys: string[] = []

if (Node.isUnionTypeNode(keysType)) {
for (const unionType of keysType.getTypeNodes()) {
if (Node.isLiteralTypeNode(unionType)) {
const literalExpression = unionType.getLiteral()
if (Node.isStringLiteral(literalExpression)) {
keys.push(literalExpression.getLiteralText())
}
}
}
} else if (Node.isLiteralTypeNode(keysType)) {
const literalExpression = keysType.getLiteral()
if (Node.isStringLiteral(literalExpression)) {
keys.push(literalExpression.getLiteralText())
}
}

return keys
}

/**
* Converts an array of string keys into a TypeBox expression
* Returns a single Literal for one key, or a Union of Literals for multiple keys
*/
export const createTypeBoxKeys = (keys: string[]): ts.Expression => {
if (keys.length === 1) {
return makeTypeCall('Literal', [ts.factory.createStringLiteral(keys[0]!)])
}

return makeTypeCall('Union', [
ts.factory.createArrayLiteralExpression(
keys.map((k) => makeTypeCall('Literal', [ts.factory.createStringLiteral(k)])),
false,
),
])
}
70 changes: 70 additions & 0 deletions src/utils/node-type-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Node, SyntaxKind, TypeReferenceNode } from 'ts-morph'

/**
* Utility functions for common Node type checks used in canHandle methods
*/

/**
* Checks if a node is a specific SyntaxKind
*/
export const isSyntaxKind = (node: Node, kind: SyntaxKind): boolean => {
return node.getKind() === kind
}

/**
* Checks if a node is any of the specified SyntaxKinds
*/
export const isAnySyntaxKind = (node: Node, kinds: readonly SyntaxKind[]): boolean => {
return kinds.includes(node.getKind())
}

/**
* Checks if a node is a TypeOperatorTypeNode with a specific operator
*/
export const isTypeOperatorWithOperator = (node: Node, operator: SyntaxKind): boolean => {
return Node.isTypeOperatorTypeNode(node) && node.getOperator() === operator
}

/**
* Checks if a node is a TypeReference with a specific type name
*/
export const isTypeReferenceWithName = (node: Node, typeName: string): boolean => {
if (!Node.isTypeReference(node)) {
return false
}

const typeRefNode = node as TypeReferenceNode
const typeNameNode = typeRefNode.getTypeName()

return Node.isIdentifier(typeNameNode) && typeNameNode.getText() === typeName
}

/**
* Checks if a node is a TypeReference with any of the specified type names
*/
export const isTypeReferenceWithAnyName = (node: Node, typeNames: string[]): boolean => {
if (!Node.isTypeReference(node)) {
return false
}

const typeRefNode = node as TypeReferenceNode
const typeNameNode = typeRefNode.getTypeName()

return Node.isIdentifier(typeNameNode) && typeNames.includes(typeNameNode.getText())
}

/**
* Utility type operators
*/
export const UTILITY_TYPE_NAMES = [
'Partial',
'Required',
'Readonly',
'Pick',
'Omit',
'Exclude',
'Extract',
'NonNullable',
'ReturnType',
'InstanceType',
] as const