Skip to content

Commit 12ca1af

Browse files
authored
feat(typebox): add support for keyof typeof expressions (#18)
The changes in this commit add support for handling `keyof typeof` expressions in the TypeBox type handler system. This is an important feature as `keyof typeof` is a common TypeScript construct used to extract the keys of an object type. The key changes are: - Added a new `KeyOfTypeofHandler` class that extends the `BaseTypeHandler` and handles `keyof typeof` expressions. - Integrated the `KeyOfTypeofHandler` into the overall handler system, ensuring it is prioritized over the regular `KeyOfTypeHandler`. - Added test cases to cover various scenarios for `keyof typeof` handling, including single string, single numeric, multiple string, multiple numeric, and mixed properties. These changes will allow the TypeBox type handler to correctly generate the appropriate TypeBox types for `keyof typeof` expressions, improving the overall functionality and robustness of the validation schema codegen tool.
1 parent 2866a64 commit 12ca1af

File tree

5 files changed

+218
-0
lines changed

5 files changed

+218
-0
lines changed

docs/handler-system.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export abstract class BaseTypeHandler {
4242

4343
- `TemplateLiteralTypeHandler` - `template ${string}`
4444
- `KeyofTypeHandler` - keyof T
45+
- `KeyOfTypeofHandler` - keyof typeof obj
4546
- `IndexedAccessTypeHandler` - T[K]
4647

4748
## Handler Management

docs/overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Transforms TypeScript types to TypeBox schemas.
1111
- Union/intersection types
1212
- Utility types (Pick, Omit, Partial, Required, Record, Readonly)
1313
- Template literal types
14+
- keyof typeof expressions
1415
- Date type
1516

1617
## Usage
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler'
2+
import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
3+
import { Node, ts, TypeOperatorTypeNode, VariableDeclaration } from 'ts-morph'
4+
5+
export class KeyOfTypeofHandler extends BaseTypeHandler {
6+
canHandle(node: Node): boolean {
7+
return (
8+
Node.isTypeOperatorTypeNode(node) &&
9+
node.getOperator() === ts.SyntaxKind.KeyOfKeyword &&
10+
Node.isTypeQuery(node.getTypeNode())
11+
)
12+
}
13+
14+
handle(node: TypeOperatorTypeNode): ts.Expression {
15+
const typeQuery = node.getTypeNode()
16+
if (!Node.isTypeQuery(typeQuery)) return makeTypeCall('Any')
17+
18+
const exprName = typeQuery.getExprName()
19+
if (!Node.isIdentifier(exprName)) return makeTypeCall('Any')
20+
21+
const keys = this.getObjectKeys(exprName)
22+
return keys.length > 0 ? this.createUnion(keys) : makeTypeCall('Any')
23+
}
24+
25+
private getObjectKeys(node: Node): string[] {
26+
const sourceFile = node.getSourceFile()
27+
28+
for (const varDecl of sourceFile.getVariableDeclarations()) {
29+
if (varDecl.getName() === node.getText()) {
30+
return this.extractKeys(varDecl)
31+
}
32+
}
33+
34+
return []
35+
}
36+
37+
private extractKeys(varDecl: VariableDeclaration): string[] {
38+
// Try object literal
39+
let initializer = varDecl.getInitializer()
40+
if (Node.isAsExpression(initializer)) {
41+
initializer = initializer.getExpression()
42+
}
43+
44+
if (Node.isObjectLiteralExpression(initializer)) {
45+
return initializer
46+
.getProperties()
47+
.map((prop) => {
48+
if (Node.isPropertyAssignment(prop) || Node.isShorthandPropertyAssignment(prop)) {
49+
const name = prop.getName()
50+
return typeof name === 'string' ? name : null
51+
}
52+
return null
53+
})
54+
.filter((name): name is string => name !== null)
55+
}
56+
57+
// Try type annotation
58+
const typeNode = varDecl.getTypeNode()
59+
if (Node.isTypeLiteral(typeNode)) {
60+
return typeNode
61+
.getMembers()
62+
.map((member) => {
63+
if (Node.isPropertySignature(member)) {
64+
const name = member.getName()
65+
return typeof name === 'string' ? name : null
66+
}
67+
return null
68+
})
69+
.filter((name): name is string => name !== null)
70+
}
71+
72+
return []
73+
}
74+
75+
private createUnion(keys: string[]): ts.Expression {
76+
const literals = keys.map((key) => {
77+
const num = Number(key)
78+
const literal =
79+
!isNaN(num) && key === String(num)
80+
? ts.factory.createNumericLiteral(num)
81+
: ts.factory.createStringLiteral(key)
82+
83+
return makeTypeCall('Literal', [literal])
84+
})
85+
86+
return literals.length === 1
87+
? literals[0]!
88+
: makeTypeCall('Union', [ts.factory.createArrayLiteralExpression(literals)])
89+
}
90+
}

src/handlers/typebox/typebox-type-handlers.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { DateTypeHandler } from '@daxserver/validation-schema-codegen/handlers/t
77
import { FunctionTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/function-type-handler'
88
import { IndexedAccessTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/indexed-access-type-handler'
99
import { KeyOfTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/keyof-type-handler'
10+
import { KeyOfTypeofHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/keyof-typeof-handler'
1011
import { LiteralTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/literal-type-handler'
1112
import { InterfaceTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/object/interface-type-handler'
1213
import { ObjectTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/object/object-type-handler'
@@ -45,6 +46,7 @@ export class TypeBoxTypeHandlers {
4546
const requiredTypeHandler = new RequiredTypeHandler()
4647
const typeReferenceHandler = new TypeReferenceHandler()
4748
const keyOfTypeHandler = new KeyOfTypeHandler()
49+
const keyOfTypeofHandler = new KeyOfTypeofHandler()
4850
const indexedAccessTypeHandler = new IndexedAccessTypeHandler()
4951
const interfaceTypeHandler = new InterfaceTypeHandler()
5052
const functionTypeHandler = new FunctionTypeHandler()
@@ -89,6 +91,7 @@ export class TypeBoxTypeHandlers {
8991

9092
// Fallback handlers for complex cases
9193
this.fallbackHandlers = [
94+
keyOfTypeofHandler, // Must come before keyOfTypeHandler
9295
typeReferenceHandler,
9396
keyOfTypeHandler,
9497
typeofTypeHandler,
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { createSourceFile, formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils'
2+
import { beforeEach, describe, expect, test } from 'bun:test'
3+
import { Project } from 'ts-morph'
4+
5+
describe('KeyOf typeof handling', () => {
6+
let project: Project
7+
8+
beforeEach(() => {
9+
project = new Project()
10+
})
11+
12+
test('should handle single string', () => {
13+
const sourceFile = createSourceFile(
14+
project,
15+
`
16+
const A = {
17+
a: 'a',
18+
}
19+
type T = keyof typeof A
20+
`,
21+
)
22+
23+
expect(generateFormattedCode(sourceFile)).toBe(
24+
formatWithPrettier(`
25+
export const T = Type.Literal("a");
26+
27+
export type T = Static<typeof T>;
28+
`),
29+
)
30+
})
31+
32+
test('shoud handle single numeric', () => {
33+
const sourceFile = createSourceFile(
34+
project,
35+
`
36+
const A = {
37+
0: 'a',
38+
}
39+
type T = keyof typeof A
40+
`,
41+
)
42+
43+
expect(generateFormattedCode(sourceFile)).toBe(
44+
formatWithPrettier(`
45+
export const T = Type.Literal(0);
46+
47+
export type T = Static<typeof T>;
48+
`),
49+
)
50+
})
51+
52+
test('should handle multiple string properties', () => {
53+
const sourceFile = createSourceFile(
54+
project,
55+
`
56+
const A = {
57+
a: 'a',
58+
b: 'b',
59+
}
60+
type T = keyof typeof A
61+
`,
62+
)
63+
64+
expect(generateFormattedCode(sourceFile)).toBe(
65+
formatWithPrettier(`
66+
export const T = Type.Union([
67+
Type.Literal("a"),
68+
Type.Literal("b"),
69+
]);
70+
71+
export type T = Static<typeof T>;
72+
`),
73+
)
74+
})
75+
76+
test('should handle multiple numeric properties', () => {
77+
const sourceFile = createSourceFile(
78+
project,
79+
`
80+
const A = {
81+
0: 'a',
82+
1: 'b',
83+
};
84+
type T = keyof typeof A
85+
`,
86+
)
87+
88+
expect(generateFormattedCode(sourceFile)).toBe(
89+
formatWithPrettier(`
90+
export const T = Type.Union([
91+
Type.Literal(0),
92+
Type.Literal(1),
93+
]);
94+
95+
export type T = Static<typeof T>;
96+
`),
97+
)
98+
})
99+
100+
test('should handle mixed properties', () => {
101+
const sourceFile = createSourceFile(
102+
project,
103+
`
104+
const A = {
105+
a: 'a',
106+
0: 'b',
107+
};
108+
type T = keyof typeof A
109+
`,
110+
)
111+
112+
expect(generateFormattedCode(sourceFile)).toBe(
113+
formatWithPrettier(`
114+
export const T = Type.Union([
115+
Type.Literal("a"),
116+
Type.Literal(0),
117+
]);
118+
119+
export type T = Static<typeof T>;
120+
`),
121+
)
122+
})
123+
})

0 commit comments

Comments
 (0)