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