Skip to content

Commit 88a99d4

Browse files
authored
refactor: extract string keys utility and optimize omit type handler (#10)
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.
1 parent c12aa8d commit 88a99d4

File tree

9 files changed

+189
-99
lines changed

9 files changed

+189
-99
lines changed

ARCHITECTURE.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ const result = await generateCode({
196196
- <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.
197197
- <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.
198198
- <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.
199+
- <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.
200+
- <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.
199201

200202
### Handlers Directory
201203

@@ -207,6 +209,7 @@ This directory contains a collection of specialized handler modules, each respon
207209
- <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.
208210
- <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.
209211
- <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.
212+
- <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.
210213

211214
#### Type Handler Implementations
212215

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

236+
**Type Operator Handlers** (extend `TypeOperatorBaseHandler`):
237+
238+
- <mcfile name="keyof-type-handler.ts" path="src/handlers/typebox/keyof-type-handler.ts"></mcfile>: Handles TypeScript `keyof` type operator for extracting object keys.
239+
- <mcfile name="readonly-type-handler.ts" path="src/handlers/typebox/readonly-type-handler.ts"></mcfile>: Handles TypeScript `readonly` type modifier for creating immutable types.
240+
233241
**Standalone Type Handlers** (extend `BaseTypeHandler`):
234242

235243
- <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`.
236244
- <mcfile name="literal-type-handler.ts" path="src/handlers/typebox/literal-type-handler.ts"></mcfile>: Handles TypeScript literal types (e.g., `'hello'`, `123`, `true`).
237245
- <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.
238246
- <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.
239247
- <mcfile name="typeof-type-handler.ts" path="src/handlers/typebox/typeof-type-handler.ts"></mcfile>: Handles TypeScript `typeof` expressions for extracting types from values.
240-
- <mcfile name="keyof-type-handler.ts" path="src/handlers/typebox/keyof-type-handler.ts"></mcfile>: Handles TypeScript `keyof` type operator for extracting object keys.
241-
- <mcfile name="readonly-type-handler.ts" path="src/handlers/typebox/readonly-type-handler.ts"></mcfile>: Handles TypeScript `readonly` type modifier for creating immutable types.
242248
- <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.
243249
- <mcfile name="type-reference-handler.ts" path="src/handlers/typebox/type-reference-handler.ts"></mcfile>: Handles references to other types (e.g., `MyType`).
244250
- <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]`).
Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
1-
import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler'
2-
import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call'
1+
import { TypeOperatorBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/type-operator-base-handler'
32
import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
4-
import { Node, SyntaxKind, ts, TypeOperatorTypeNode } from 'ts-morph'
3+
import { SyntaxKind, ts } from 'ts-morph'
54

6-
export class KeyOfTypeHandler extends BaseTypeHandler {
7-
canHandle(node: Node): boolean {
8-
return Node.isTypeOperatorTypeNode(node) && node.getOperator() === SyntaxKind.KeyOfKeyword
9-
}
10-
11-
handle(node: TypeOperatorTypeNode): ts.Expression {
12-
const operandType = node.getTypeNode()
13-
const typeboxOperand = getTypeBoxType(operandType)
5+
export class KeyOfTypeHandler extends TypeOperatorBaseHandler {
6+
protected readonly operatorKind = SyntaxKind.KeyOfKeyword
7+
protected readonly typeBoxMethod = 'KeyOf'
148

15-
return makeTypeCall('KeyOf', [typeboxOperand])
9+
protected createTypeBoxCall(innerType: ts.Expression): ts.Expression {
10+
return makeTypeCall('KeyOf', [innerType])
1611
}
1712
}
Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
1-
import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler'
2-
import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call'
1+
import { TypeOperatorBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/type-operator-base-handler'
32
import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
4-
import { Node, SyntaxKind, ts, TypeOperatorTypeNode } from 'ts-morph'
3+
import { SyntaxKind, ts } from 'ts-morph'
54

6-
export class ReadonlyTypeHandler extends BaseTypeHandler {
7-
canHandle(node: Node): boolean {
8-
return Node.isTypeOperatorTypeNode(node) && node.getOperator() === SyntaxKind.ReadonlyKeyword
9-
}
10-
11-
handle(node: TypeOperatorTypeNode): ts.Expression {
12-
const operandType = node.getTypeNode()
13-
const typeboxOperand = getTypeBoxType(operandType)
5+
export class ReadonlyTypeHandler extends TypeOperatorBaseHandler {
6+
protected readonly operatorKind = SyntaxKind.ReadonlyKeyword
7+
protected readonly typeBoxMethod = 'Readonly'
148

15-
// TypeBox uses Readonly utility type for readonly modifiers
16-
return makeTypeCall('Readonly', [typeboxOperand])
9+
protected createTypeBoxCall(innerType: ts.Expression): ts.Expression {
10+
return makeTypeCall('Readonly', [innerType])
1711
}
1812
}

src/handlers/typebox/reference/omit-type-handler.ts

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { TypeReferenceBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/reference/type-reference-base-handler'
2+
import {
3+
createTypeBoxKeys,
4+
extractStringKeys,
5+
} from '@daxserver/validation-schema-codegen/utils/key-extraction-utils'
26
import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call'
37
import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
48
import { Node, ts } from 'ts-morph'
@@ -16,36 +20,8 @@ export class OmitTypeHandler extends TypeReferenceBaseHandler {
1620
}
1721

1822
const typeboxObjectType = getTypeBoxType(objectType)
19-
20-
let omitKeys: string[] = []
21-
if (Node.isUnionTypeNode(keysType)) {
22-
omitKeys = keysType.getTypeNodes().map((unionType) => {
23-
if (Node.isLiteralTypeNode(unionType)) {
24-
const literalExpression = unionType.getLiteral()
25-
if (Node.isStringLiteral(literalExpression)) {
26-
return literalExpression.getLiteralText()
27-
}
28-
}
29-
return '' // Should not happen if keys are string literals
30-
})
31-
} else if (Node.isLiteralTypeNode(keysType)) {
32-
const literalExpression = keysType.getLiteral()
33-
if (Node.isStringLiteral(literalExpression)) {
34-
omitKeys = [literalExpression.getLiteralText()]
35-
}
36-
}
37-
38-
let typeboxKeys: ts.Expression
39-
if (omitKeys.length === 1) {
40-
typeboxKeys = makeTypeCall('Literal', [ts.factory.createStringLiteral(omitKeys[0]!)])
41-
} else {
42-
typeboxKeys = makeTypeCall('Union', [
43-
ts.factory.createArrayLiteralExpression(
44-
omitKeys.map((k) => makeTypeCall('Literal', [ts.factory.createStringLiteral(k)])),
45-
true,
46-
),
47-
])
48-
}
23+
const omitKeys = extractStringKeys(keysType)
24+
const typeboxKeys = createTypeBoxKeys(omitKeys)
4925

5026
return makeTypeCall('Omit', [typeboxObjectType, typeboxKeys])
5127
}

src/handlers/typebox/reference/pick-type-handler.ts

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { TypeReferenceBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/reference/type-reference-base-handler'
2+
import {
3+
createTypeBoxKeys,
4+
extractStringKeys,
5+
} from '@daxserver/validation-schema-codegen/utils/key-extraction-utils'
26
import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call'
37
import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
48
import { Node, ts } from 'ts-morph'
@@ -16,36 +20,8 @@ export class PickTypeHandler extends TypeReferenceBaseHandler {
1620
}
1721

1822
const typeboxObjectType = getTypeBoxType(objectType)
19-
20-
let pickKeys: string[] = []
21-
if (Node.isUnionTypeNode(keysType)) {
22-
pickKeys = keysType.getTypeNodes().map((unionType) => {
23-
if (Node.isLiteralTypeNode(unionType)) {
24-
const literalExpression = unionType.getLiteral()
25-
if (Node.isStringLiteral(literalExpression)) {
26-
return literalExpression.getLiteralText()
27-
}
28-
}
29-
return '' // Should not happen if keys are string literals
30-
})
31-
} else if (Node.isLiteralTypeNode(keysType)) {
32-
const literalExpression = keysType.getLiteral()
33-
if (Node.isStringLiteral(literalExpression)) {
34-
pickKeys = [literalExpression.getLiteralText()]
35-
}
36-
}
37-
38-
let typeboxKeys: ts.Expression
39-
if (pickKeys.length === 1) {
40-
typeboxKeys = makeTypeCall('Literal', [ts.factory.createStringLiteral(pickKeys[0]!)])
41-
} else {
42-
typeboxKeys = makeTypeCall('Union', [
43-
ts.factory.createArrayLiteralExpression(
44-
pickKeys.map((k) => makeTypeCall('Literal', [ts.factory.createStringLiteral(k)])),
45-
false,
46-
),
47-
])
48-
}
23+
const pickKeys = extractStringKeys(keysType)
24+
const typeboxKeys = createTypeBoxKeys(pickKeys)
4925

5026
return makeTypeCall('Pick', [typeboxObjectType, typeboxKeys])
5127
}
Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler'
2+
import { isAnySyntaxKind } from '@daxserver/validation-schema-codegen/utils/node-type-utils'
23
import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
34
import { Node, SyntaxKind, ts } from 'ts-morph'
45

56
export const TypeBoxType = 'Type'
67

7-
type SimpleKinds =
8-
| SyntaxKind.AnyKeyword
9-
| SyntaxKind.BooleanKeyword
10-
| SyntaxKind.NeverKeyword
11-
| SyntaxKind.NullKeyword
12-
| SyntaxKind.NumberKeyword
13-
| SyntaxKind.StringKeyword
14-
| SyntaxKind.UnknownKeyword
15-
| SyntaxKind.VoidKeyword
8+
const SimpleKinds = [
9+
SyntaxKind.AnyKeyword,
10+
SyntaxKind.BooleanKeyword,
11+
SyntaxKind.NeverKeyword,
12+
SyntaxKind.NullKeyword,
13+
SyntaxKind.NumberKeyword,
14+
SyntaxKind.StringKeyword,
15+
SyntaxKind.UnknownKeyword,
16+
SyntaxKind.VoidKeyword,
17+
] as const
18+
type SimpleKind = (typeof SimpleKinds)[number]
1619

17-
const kindToTypeBox: Record<SimpleKinds, string> = {
20+
const kindToTypeBox: Record<SimpleKind, string> = {
1821
[SyntaxKind.AnyKeyword]: 'Any',
1922
[SyntaxKind.BooleanKeyword]: 'Boolean',
2023
[SyntaxKind.NeverKeyword]: 'Never',
@@ -27,10 +30,10 @@ const kindToTypeBox: Record<SimpleKinds, string> = {
2730

2831
export class SimpleTypeHandler extends BaseTypeHandler {
2932
canHandle(node: Node): boolean {
30-
return node.getKind() in kindToTypeBox
33+
return isAnySyntaxKind(node, SimpleKinds)
3134
}
3235

3336
handle(node: Node): ts.Expression {
34-
return makeTypeCall(kindToTypeBox[node.getKind() as SimpleKinds])
37+
return makeTypeCall(kindToTypeBox[node.getKind() as SimpleKind])
3538
}
3639
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler'
2+
import { isTypeOperatorWithOperator } from '@daxserver/validation-schema-codegen/utils/node-type-utils'
3+
import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call'
4+
import { Node, SyntaxKind, ts, TypeOperatorTypeNode } from 'ts-morph'
5+
6+
/**
7+
* Base class for TypeOperator handlers (KeyOf, Readonly, etc.)
8+
* Provides common functionality for handling TypeOperatorTypeNode
9+
*/
10+
export abstract class TypeOperatorBaseHandler extends BaseTypeHandler {
11+
protected abstract readonly operatorKind: SyntaxKind
12+
protected abstract readonly typeBoxMethod: string
13+
14+
canHandle(node: Node): boolean {
15+
return isTypeOperatorWithOperator(node, this.operatorKind)
16+
}
17+
18+
handle(node: TypeOperatorTypeNode): ts.Expression {
19+
const innerType = node.getTypeNode()
20+
const typeboxInnerType = getTypeBoxType(innerType)
21+
return this.createTypeBoxCall(typeboxInnerType)
22+
}
23+
24+
protected abstract createTypeBoxCall(innerType: ts.Expression): ts.Expression
25+
}

src/utils/key-extraction-utils.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
2+
import { Node, ts } from 'ts-morph'
3+
4+
/**
5+
* Extracts string keys from a TypeScript type node (typically used for Pick/Omit key types)
6+
* Handles both single literal types and union types containing string literals
7+
*/
8+
export const extractStringKeys = (keysType: Node): string[] => {
9+
const keys: string[] = []
10+
11+
if (Node.isUnionTypeNode(keysType)) {
12+
for (const unionType of keysType.getTypeNodes()) {
13+
if (Node.isLiteralTypeNode(unionType)) {
14+
const literalExpression = unionType.getLiteral()
15+
if (Node.isStringLiteral(literalExpression)) {
16+
keys.push(literalExpression.getLiteralText())
17+
}
18+
}
19+
}
20+
} else if (Node.isLiteralTypeNode(keysType)) {
21+
const literalExpression = keysType.getLiteral()
22+
if (Node.isStringLiteral(literalExpression)) {
23+
keys.push(literalExpression.getLiteralText())
24+
}
25+
}
26+
27+
return keys
28+
}
29+
30+
/**
31+
* Converts an array of string keys into a TypeBox expression
32+
* Returns a single Literal for one key, or a Union of Literals for multiple keys
33+
*/
34+
export const createTypeBoxKeys = (keys: string[]): ts.Expression => {
35+
if (keys.length === 1) {
36+
return makeTypeCall('Literal', [ts.factory.createStringLiteral(keys[0]!)])
37+
}
38+
39+
return makeTypeCall('Union', [
40+
ts.factory.createArrayLiteralExpression(
41+
keys.map((k) => makeTypeCall('Literal', [ts.factory.createStringLiteral(k)])),
42+
false,
43+
),
44+
])
45+
}

src/utils/node-type-utils.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { Node, SyntaxKind, TypeReferenceNode } from 'ts-morph'
2+
3+
/**
4+
* Utility functions for common Node type checks used in canHandle methods
5+
*/
6+
7+
/**
8+
* Checks if a node is a specific SyntaxKind
9+
*/
10+
export const isSyntaxKind = (node: Node, kind: SyntaxKind): boolean => {
11+
return node.getKind() === kind
12+
}
13+
14+
/**
15+
* Checks if a node is any of the specified SyntaxKinds
16+
*/
17+
export const isAnySyntaxKind = (node: Node, kinds: readonly SyntaxKind[]): boolean => {
18+
return kinds.includes(node.getKind())
19+
}
20+
21+
/**
22+
* Checks if a node is a TypeOperatorTypeNode with a specific operator
23+
*/
24+
export const isTypeOperatorWithOperator = (node: Node, operator: SyntaxKind): boolean => {
25+
return Node.isTypeOperatorTypeNode(node) && node.getOperator() === operator
26+
}
27+
28+
/**
29+
* Checks if a node is a TypeReference with a specific type name
30+
*/
31+
export const isTypeReferenceWithName = (node: Node, typeName: string): boolean => {
32+
if (!Node.isTypeReference(node)) {
33+
return false
34+
}
35+
36+
const typeRefNode = node as TypeReferenceNode
37+
const typeNameNode = typeRefNode.getTypeName()
38+
39+
return Node.isIdentifier(typeNameNode) && typeNameNode.getText() === typeName
40+
}
41+
42+
/**
43+
* Checks if a node is a TypeReference with any of the specified type names
44+
*/
45+
export const isTypeReferenceWithAnyName = (node: Node, typeNames: string[]): boolean => {
46+
if (!Node.isTypeReference(node)) {
47+
return false
48+
}
49+
50+
const typeRefNode = node as TypeReferenceNode
51+
const typeNameNode = typeRefNode.getTypeName()
52+
53+
return Node.isIdentifier(typeNameNode) && typeNames.includes(typeNameNode.getText())
54+
}
55+
56+
/**
57+
* Utility type operators
58+
*/
59+
export const UTILITY_TYPE_NAMES = [
60+
'Partial',
61+
'Required',
62+
'Readonly',
63+
'Pick',
64+
'Omit',
65+
'Exclude',
66+
'Extract',
67+
'NonNullable',
68+
'ReturnType',
69+
'InstanceType',
70+
] as const

0 commit comments

Comments
 (0)