From 8257706033ea8e58b79a9a046859d69a0bf00297 Mon Sep 17 00:00:00 2001 From: DaxServer Date: Tue, 26 Aug 2025 13:35:22 +0200 Subject: [PATCH] refactor: extract string keys utility and optimize omit type handler This commit introduces two main changes: 1. Adds a new utility function `extractStringKeys` in `key-extraction-utils.ts` to extract string keys from a TypeScript type node. This is useful for handling the key types in `Pick` and `Omit` operations. 2. Optimizes the `OmitTypeHandler` in `omit-type-handler.ts` by using the new `extractStringKeys` utility and the `createTypeBoxKeys` function to generate the TypeBox expression for the omitted keys. This simplifies the code and makes it more readable. The changes improve the overall code quality and maintainability of the validation schema codegen library. --- ARCHITECTURE.md | 10 ++- src/handlers/typebox/keyof-type-handler.ts | 19 ++--- src/handlers/typebox/readonly-type-handler.ts | 20 ++---- .../typebox/reference/omit-type-handler.ts | 36 ++-------- .../typebox/reference/pick-type-handler.ts | 36 ++-------- src/handlers/typebox/simple-type-handler.ts | 27 +++---- .../typebox/type-operator-base-handler.ts | 25 +++++++ src/utils/key-extraction-utils.ts | 45 ++++++++++++ src/utils/node-type-utils.ts | 70 +++++++++++++++++++ 9 files changed, 189 insertions(+), 99 deletions(-) create mode 100644 src/handlers/typebox/type-operator-base-handler.ts create mode 100644 src/utils/key-extraction-utils.ts create mode 100644 src/utils/node-type-utils.ts 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