Skip to content

Conversation

@DaxServer
Copy link
Owner

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.

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.
@coderabbitai
Copy link

coderabbitai bot commented Aug 30, 2025

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added support for generating schemas from TypeScript “keyof typeof” expressions, producing literal or union types for string, numeric, or mixed keys.
  • Documentation
    • Updated Overview to list “keyof typeof” as a supported type.
    • Refreshed Advanced documentation to reflect the updated public API terminology.
  • Tests
    • Added comprehensive tests covering single and multiple keys (string, numeric, mixed) to ensure correct schema generation and type alias output.

Walkthrough

Adds a new TypeBox handler for keyof typeof expressions, wires it into the handler pipeline with higher precedence, updates documentation to reflect the new handler and supported types, and introduces tests validating code generation for various key sets.

Changes

Cohort / File(s) Summary of Changes
Docs: Handler API and Overview
docs/handler-system.md, docs/overview.md
Updated Advanced API listing: replaced KeyofTypeHandler (keyof T) with KeyOfTypeofHandler (keyof typeof obj). Added “keyof typeof expressions” to Supported Types.
TypeBox Handler: New implementation
src/handlers/typebox/keyof-typeof-handler.ts
Introduced KeyOfTypeofHandler with canHandle and handle. Resolves keyof typeof Identifier by extracting keys from object/type literals and produces TypeBox Literal/Union; falls back to Any on invalid/no keys.
Handler orchestration
src/handlers/typebox/typebox-type-handlers.ts
Imported and instantiated KeyOfTypeofHandler. Inserted into fallbackHandlers before KeyOfTypeHandler to ensure precedence.
Tests: TypeBox keyof typeof
tests/handlers/typebox/keyof-typeof.test.ts
Added test cases for single/multiple and string/numeric/mixed keys, asserting generated TypeBox literals/unions and corresponding Static type aliases.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant CG as Codegen
  participant TH as TypeBoxTypeHandlers
  participant KTT as KeyOfTypeofHandler
  participant KTH as KeyOfTypeHandler
  participant AST as TS AST/Source

  CG->>TH: request type for node
  TH->>KTT: canHandle(node)?
  alt node is "keyof typeof Identifier"
    KTT->>AST: resolve Identifier declaration
    alt extract keys succeeds
      KTT-->>TH: Type.Union/Literal of keys
    else no keys/invalid
      KTT-->>TH: Type.Any
    end
  else not applicable
    TH->>KTH: canHandle(node)?
    KTH-->>TH: handle or pass
  end
  TH-->>CG: TypeBox expression
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • refactor: re-organize handlers #6 — Also modifies TypeBox keyof/typeof-related handlers and their orchestration, likely preceding or complementing this handler addition.
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch keyof-typeof

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (6)
docs/overview.md (1)

14-14: Code-format TS keywords for consistency.

Render TS syntax as code to match the rest of the docs.

- - keyof typeof expressions
+ - `keyof typeof` expressions
docs/handler-system.md (1)

45-46: Clarify handler precedence in docs.

Call out that KeyOfTypeofHandler is evaluated before KeyofTypeHandler to avoid confusion.

-- `KeyofTypeHandler` - keyof T
-- `KeyOfTypeofHandler` - keyof typeof obj
+- `KeyofTypeHandler` - keyof T
+- `KeyOfTypeofHandler` - keyof typeof obj (evaluated before `KeyofTypeHandler`)
src/handlers/typebox/keyof-typeof-handler.ts (2)

38-45: Unwrap common wrappers (as, satisfies, parens) before probing initializer.

This improves robustness for patterns like const A = ({ a: 1 }) as const or using satisfies.

-    // Try object literal
-    let initializer = varDecl.getInitializer()
-    if (Node.isAsExpression(initializer)) {
-      initializer = initializer.getExpression()
-    }
+    // Try object literal
+    let initializer = varDecl.getInitializer()
+    while (
+      initializer &&
+      (Node.isAsExpression(initializer) ||
+        Node.isSatisfiesExpression?.(initializer) ||
+        Node.isParenthesizedExpression(initializer))
+    ) {
+      initializer = initializer.getExpression()
+    }

18-21: Support qualified names (typeof ns.A).

getExprName() can be a qualified name. Consider accepting QualifiedName and using the rightmost identifier or symbol resolution to locate declarations (including imports).

tests/handlers/typebox/keyof-typeof.test.ts (2)

32-32: Fix typo in test name.

-  test('shoud handle single numeric', () => {
+  test('should handle single numeric', () => {

123-123: Add a test for empty objects (should be Never).

This guards the keyof {} edge case.

+  test('should handle empty object as Never', () => {
+    const sourceFile = createSourceFile(
+      project,
+      `
+        const A = {};
+        type T = keyof typeof A
+      `,
+    )
+
+    expect(generateFormattedCode(sourceFile)).toBe(
+      formatWithPrettier(`
+        export const T = Type.Never();
+
+        export type T = Static<typeof T>;
+      `),
+    )
+  })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2866a64 and 6f84073.

📒 Files selected for processing (5)
  • docs/handler-system.md (1 hunks)
  • docs/overview.md (1 hunks)
  • src/handlers/typebox/keyof-typeof-handler.ts (1 hunks)
  • src/handlers/typebox/typebox-type-handlers.ts (3 hunks)
  • tests/handlers/typebox/keyof-typeof.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
tests/handlers/typebox/keyof-typeof.test.ts (1)
tests/utils.ts (3)
  • createSourceFile (12-14)
  • generateFormattedCode (26-37)
  • formatWithPrettier (16-24)
src/handlers/typebox/typebox-type-handlers.ts (1)
src/handlers/typebox/keyof-typeof-handler.ts (1)
  • KeyOfTypeofHandler (5-90)
src/handlers/typebox/keyof-typeof-handler.ts (2)
src/utils/typebox-codegen-utils.ts (1)
  • makeTypeCall (3-12)
src/handlers/typebox/keyof-type-handler.ts (1)
  • KeyOfTypeHandler (5-12)
🪛 LanguageTool
docs/overview.md

[grammar] ~14-~14: There might be a mistake here.
Context: ...literal types - keyof typeof expressions - Date type ## Usage ```typescript impor...

(QB_NEW_EN)

🔇 Additional comments (1)
src/handlers/typebox/typebox-type-handlers.ts (1)

10-10: Wiring and precedence look correct.

Import, instantiation, and placement before KeyofTypeHandler in fallbacks are spot on.

Also applies to: 49-49, 94-99

Comment on lines +14 to +23
handle(node: TypeOperatorTypeNode): ts.Expression {
const typeQuery = node.getTypeNode()
if (!Node.isTypeQuery(typeQuery)) return makeTypeCall('Any')

const exprName = typeQuery.getExprName()
if (!Node.isIdentifier(exprName)) return makeTypeCall('Any')

const keys = this.getObjectKeys(exprName)
return keys.length > 0 ? this.createUnion(keys) : makeTypeCall('Any')
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Empty objects should yield Never, not Any.

keyof {} is never. Your current [] -> Any conflates “resolved empty” with “unresolved”. Return Any only when the target couldn’t be resolved; return Never for resolved-but-empty.

-  handle(node: TypeOperatorTypeNode): ts.Expression {
+  handle(node: TypeOperatorTypeNode): ts.Expression {
     const typeQuery = node.getTypeNode()
-    if (!Node.isTypeQuery(typeQuery)) return makeTypeCall('Any')
+    if (!Node.isTypeQuery(typeQuery)) return makeTypeCall('Any')
 
     const exprName = typeQuery.getExprName()
-    if (!Node.isIdentifier(exprName)) return makeTypeCall('Any')
+    if (!Node.isIdentifier(exprName)) return makeTypeCall('Any')
 
-    const keys = this.getObjectKeys(exprName)
-    return keys.length > 0 ? this.createUnion(keys) : makeTypeCall('Any')
+    const keys = this.getObjectKeys(exprName)
+    if (keys === null) return makeTypeCall('Any')
+    if (keys.length === 0) return makeTypeCall('Never')
+    return this.createUnion(keys)
   }
 
-  private getObjectKeys(node: Node): string[] {
+  private getObjectKeys(node: Node): string[] | null {
     const sourceFile = node.getSourceFile()
 
     for (const varDecl of sourceFile.getVariableDeclarations()) {
       if (varDecl.getName() === node.getText()) {
         return this.extractKeys(varDecl)
       }
     }
 
-    return []
+    return null
   }

Also applies to: 25-35

🤖 Prompt for AI Agents
In src/handlers/typebox/keyof-typeof-handler.ts around lines 14-23 (and
similarly for 25-35), the handler currently treats an empty keys array as
unresolved and returns Any; instead, return Never for a resolved-but-empty
object and only return Any when resolution truly failed. Modify getObjectKeys
(or its contract) so it returns a distinct sentinel (e.g., null) when the target
could not be resolved; then update this handler to: if keys === null return
makeTypeCall('Any'); else if keys.length === 0 return makeTypeCall('Never');
otherwise return createUnion(keys). Apply the same fix to the second block at
lines 25-35.

Comment on lines +75 to +89
private createUnion(keys: string[]): ts.Expression {
const literals = keys.map((key) => {
const num = Number(key)
const literal =
!isNaN(num) && key === String(num)
? ts.factory.createNumericLiteral(num)
: ts.factory.createStringLiteral(key)

return makeTypeCall('Literal', [literal])
})

return literals.length === 1
? literals[0]!
: makeTypeCall('Union', [ts.factory.createArrayLiteralExpression(literals)])
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Incorrect: numeric object keys should produce string literal key types.

In TypeScript, property names are strings; keyof typeof { 0: 'a' } is "0", not 0. Emitting Type.Literal(0) is semantically wrong.

Apply this diff to always emit string literals for object keys:

-  private createUnion(keys: string[]): ts.Expression {
-    const literals = keys.map((key) => {
-      const num = Number(key)
-      const literal =
-        !isNaN(num) && key === String(num)
-          ? ts.factory.createNumericLiteral(num)
-          : ts.factory.createStringLiteral(key)
-
-      return makeTypeCall('Literal', [literal])
-    })
+  private createUnion(keys: string[]): ts.Expression {
+    const literals = keys.map((key) => {
+      const literal = ts.factory.createStringLiteral(key)
+      return makeTypeCall('Literal', [literal])
+    })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private createUnion(keys: string[]): ts.Expression {
const literals = keys.map((key) => {
const num = Number(key)
const literal =
!isNaN(num) && key === String(num)
? ts.factory.createNumericLiteral(num)
: ts.factory.createStringLiteral(key)
return makeTypeCall('Literal', [literal])
})
return literals.length === 1
? literals[0]!
: makeTypeCall('Union', [ts.factory.createArrayLiteralExpression(literals)])
}
private createUnion(keys: string[]): ts.Expression {
const literals = keys.map((key) => {
const literal = ts.factory.createStringLiteral(key)
return makeTypeCall('Literal', [literal])
})
return literals.length === 1
? literals[0]!
: makeTypeCall('Union', [ts.factory.createArrayLiteralExpression(literals)])
}
🤖 Prompt for AI Agents
In src/handlers/typebox/keyof-typeof-handler.ts around lines 75 to 89, the code
currently converts numeric-looking object keys into numeric literals which is
incorrect because object property keys are always strings; update the
createUnion function to always create string literals for keys (i.e. use
ts.factory.createStringLiteral(key) for every key) instead of conditionally
creating NumericLiteral for numeric-looking keys, and keep the rest of the logic
that wraps literals in Type.Literal and combines them with Type.Union unchanged.

Comment on lines +43 to +49
expect(generateFormattedCode(sourceFile)).toBe(
formatWithPrettier(`
export const T = Type.Literal(0);

export type T = Static<typeof T>;
`),
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Adjust expectation: numeric object keys are string key types.

keyof typeof { 0: 'a' } should yield "0".

-        export const T = Type.Literal(0);
+        export const T = Type.Literal("0");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(generateFormattedCode(sourceFile)).toBe(
formatWithPrettier(`
export const T = Type.Literal(0);
export type T = Static<typeof T>;
`),
)
expect(generateFormattedCode(sourceFile)).toBe(
formatWithPrettier(`
export const T = Type.Literal("0");
export type T = Static<typeof T>;
`),
)
🤖 Prompt for AI Agents
In tests/handlers/typebox/keyof-typeof.test.ts around lines 43 to 49, the test
expectation uses a numeric literal key (Type.Literal(0)) but JavaScript object
keys that are numeric become string keys, so the expected generated type should
be the string literal "0"; update the expected formatted code to use
Type.Literal("0") (and corresponding Static<typeof T> remains) so the assertion
matches keyof typeof { 0: 'a' } producing the string key type.

Comment on lines +88 to +96
expect(generateFormattedCode(sourceFile)).toBe(
formatWithPrettier(`
export const T = Type.Union([
Type.Literal(0),
Type.Literal(1),
]);

export type T = Static<typeof T>;
`),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Adjust expectation for multiple numeric keys: use strings.

-        export const T = Type.Union([
-          Type.Literal(0),
-          Type.Literal(1),
-        ]);
+        export const T = Type.Union([
+          Type.Literal("0"),
+          Type.Literal("1"),
+        ]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(generateFormattedCode(sourceFile)).toBe(
formatWithPrettier(`
export const T = Type.Union([
Type.Literal(0),
Type.Literal(1),
]);
export type T = Static<typeof T>;
`),
expect(generateFormattedCode(sourceFile)).toBe(
formatWithPrettier(`
export const T = Type.Union([
Type.Literal("0"),
Type.Literal("1"),
]);
export type T = Static<typeof T>;
`),
🤖 Prompt for AI Agents
In tests/handlers/typebox/keyof-typeof.test.ts around lines 88–96, the expected
snapshot currently uses numeric literal types for multiple numeric keys; update
the expectation to use string literal keys instead. Replace the numeric
Type.Literal(0) and Type.Literal(1) (and any corresponding numeric type usages)
with Type.Literal("0") and Type.Literal("1") so the test matches the codegen
behavior that emits numeric keys as strings when there are multiple numeric
keys.

Comment on lines +112 to +118
expect(generateFormattedCode(sourceFile)).toBe(
formatWithPrettier(`
export const T = Type.Union([
Type.Literal("a"),
Type.Literal(0),
]);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Adjust expectation for mixed keys: numeric key should be string literal.

-        export const T = Type.Union([
-          Type.Literal("a"),
-          Type.Literal(0),
-        ]);
+        export const T = Type.Union([
+          Type.Literal("a"),
+          Type.Literal("0"),
+        ]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(generateFormattedCode(sourceFile)).toBe(
formatWithPrettier(`
export const T = Type.Union([
Type.Literal("a"),
Type.Literal(0),
]);
expect(generateFormattedCode(sourceFile)).toBe(
formatWithPrettier(`
export const T = Type.Union([
Type.Literal("a"),
Type.Literal("0"),
]);
🤖 Prompt for AI Agents
In tests/handlers/typebox/keyof-typeof.test.ts around lines 112 to 118, the test
expectation treats a mixed numeric key as a numeric literal but the generator
returns it as a string literal; update the expected formatted output so the
numeric key is represented as a string literal (e.g. Type.Literal("0")) instead
of a numeric literal, adjusting the snapshot/expect string accordingly and
re-run tests.

@DaxServer DaxServer merged commit 12ca1af into main Aug 31, 2025
7 checks passed
@DaxServer DaxServer deleted the keyof-typeof branch August 31, 2025 08:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants