diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 12f1076..bda861c 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -196,6 +196,8 @@ const result = await generateCode({
- : 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.
- : Generates and adds the `export type [TypeName] = Static` 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.
- : Contains general utility functions that support the TypeBox code generation process, such as helper functions for string manipulation or AST node creation.
+- : Provides shared utilities for extracting string literal keys from union or literal types, used by Pick and Omit type handlers to avoid code duplication.
+- : Contains common Node type checking utilities for `canHandle` methods, including functions for checking SyntaxKind, TypeOperator patterns, and TypeReference patterns.
### Handlers Directory
@@ -207,6 +209,7 @@ This directory contains a collection of specialized handler modules, each respon
- : 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`, `Pick`, etc.
- : 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.
- : 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.
+- : 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
@@ -230,6 +233,11 @@ This directory contains a collection of specialized handler modules, each respon
- : Handles TypeScript union types (e.g., `string | number`).
- : Handles TypeScript intersection types (e.g., `TypeA & TypeB`).
+**Type Operator Handlers** (extend `TypeOperatorBaseHandler`):
+
+- : Handles TypeScript `keyof` type operator for extracting object keys.
+- : Handles TypeScript `readonly` type modifier for creating immutable types.
+
**Standalone Type Handlers** (extend `BaseTypeHandler`):
- : Handles basic TypeScript types like `string`, `number`, `boolean`, `null`, `undefined`, `any`, `unknown`, `void`.
@@ -237,8 +245,6 @@ This directory contains a collection of specialized handler modules, each respon
- : Handles TypeScript function types and function declarations, including parameter types, optional parameters, and return types.
- : 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.
- : Handles TypeScript `typeof` expressions for extracting types from values.
-- : Handles TypeScript `keyof` type operator for extracting object keys.
-- : Handles TypeScript `readonly` type modifier for creating immutable types.
- : Fallback handler for other TypeScript type operators not covered by specific handlers.
- : Handles references to other types (e.g., `MyType`).
- : Handles TypeScript indexed access types (e.g., `Type[Key]`).
diff --git a/src/handlers/typebox/keyof-type-handler.ts b/src/handlers/typebox/keyof-type-handler.ts
index 331bfd0..7ba87c5 100644
--- a/src/handlers/typebox/keyof-type-handler.ts
+++ b/src/handlers/typebox/keyof-type-handler.ts
@@ -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])
}
}
diff --git a/src/handlers/typebox/readonly-type-handler.ts b/src/handlers/typebox/readonly-type-handler.ts
index 7dafcd4..745416c 100644
--- a/src/handlers/typebox/readonly-type-handler.ts
+++ b/src/handlers/typebox/readonly-type-handler.ts
@@ -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])
}
}
diff --git a/src/handlers/typebox/reference/omit-type-handler.ts b/src/handlers/typebox/reference/omit-type-handler.ts
index 497d164..0606149 100644
--- a/src/handlers/typebox/reference/omit-type-handler.ts
+++ b/src/handlers/typebox/reference/omit-type-handler.ts
@@ -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'
@@ -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)
return makeTypeCall('Omit', [typeboxObjectType, typeboxKeys])
}
diff --git a/src/handlers/typebox/reference/pick-type-handler.ts b/src/handlers/typebox/reference/pick-type-handler.ts
index 902997a..cb27d7a 100644
--- a/src/handlers/typebox/reference/pick-type-handler.ts
+++ b/src/handlers/typebox/reference/pick-type-handler.ts
@@ -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'
@@ -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)
return makeTypeCall('Pick', [typeboxObjectType, typeboxKeys])
}
diff --git a/src/handlers/typebox/simple-type-handler.ts b/src/handlers/typebox/simple-type-handler.ts
index e9fbd02..403e896 100644
--- a/src/handlers/typebox/simple-type-handler.ts
+++ b/src/handlers/typebox/simple-type-handler.ts
@@ -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 = {
+const kindToTypeBox: Record = {
[SyntaxKind.AnyKeyword]: 'Any',
[SyntaxKind.BooleanKeyword]: 'Boolean',
[SyntaxKind.NeverKeyword]: 'Never',
@@ -27,10 +30,10 @@ const kindToTypeBox: Record = {
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])
}
}
diff --git a/src/handlers/typebox/type-operator-base-handler.ts b/src/handlers/typebox/type-operator-base-handler.ts
new file mode 100644
index 0000000..3c67e6d
--- /dev/null
+++ b/src/handlers/typebox/type-operator-base-handler.ts
@@ -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)
+ }
+
+ protected abstract createTypeBoxCall(innerType: ts.Expression): ts.Expression
+}
diff --git a/src/utils/key-extraction-utils.ts b/src/utils/key-extraction-utils.ts
new file mode 100644
index 0000000..01109d7
--- /dev/null
+++ b/src/utils/key-extraction-utils.ts
@@ -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,
+ ),
+ ])
+}
diff --git a/src/utils/node-type-utils.ts b/src/utils/node-type-utils.ts
new file mode 100644
index 0000000..f5c68a6
--- /dev/null
+++ b/src/utils/node-type-utils.ts
@@ -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