Skip to content

fix(typescript-input): preserve original enum member names#3037

Merged
schani merged 3 commits into
masterfrom
agent/fix-issue-1158
Jul 21, 2026
Merged

fix(typescript-input): preserve original enum member names#3037
schani merged 3 commits into
masterfrom
agent/fix-issue-1158

Conversation

@schani

@schani schani commented Jul 20, 2026

Copy link
Copy Markdown
Member

Bug

When quicktype converts TypeScript string enums, generated code loses the original member names when the enum's value doesn't look like a valid identifier. For example:

enum TestEnum {
  Foo = "000",
  Bar = "001",
  Baz = "002"
}

produced (with --no-prefer-unions, the setting under which quicktype's TS output actually emits enum blocks):

export enum TestEnum {
    The000 = "000",
    The001 = "001",
    The002 = "002",
}

instead of the expected Foo = "000", Bar = "001", Baz = "002". The same underlying issue also mangles enum case names for other target languages (e.g. Swift), as reported in a follow-up comment on the issue.

Root cause

quicktype's TypeScript input (packages/quicktype-typescript-input) converts .ts source files into a JSON Schema via the third-party typescript-json-schema package, and that schema is then fed into quicktype-core's ordinary JSON Schema input pipeline — there's no TypeScript-specific handling downstream at all.

typescript-json-schema's enum conversion computes each member's original identifier but never writes it into the schema — only the runtime value survives (e.g. {"enum": ["000", "001", "002"]}). quicktype-core then has no choice but to synthesize a case identifier from the value itself; for "000" that synthesis falls back to prefixing "The" to produce a legal identifier. The original TS member name (Foo) is lost before it ever reaches quicktype-core.

Fix

quicktype-core already supports attaching a per-case naming hint to a string-enum schema via a "qt-accessors" object (case value → preferred identifier) — this existing mechanism (packages/quicktype-core/src/attributes/AccessorNames.ts) lets a case get a specific name while its serialized value stays untouched.

schemaForTypeScriptSources (packages/quicktype-typescript-input/src/index.ts) now walks the TypeScript program's EnumDeclaration nodes after schema generation and, for each string-valued enum, attaches a qt-accessors map from each member's value to its original TS identifier on the corresponding schema definition. No changes to quicktype-core were needed. Numeric TypeScript enums are unaffected/untouched, since quicktype doesn't currently render them as target-language enums at all.

Test coverage

This bug lives entirely in the TypeScript input conversion, which the JSON/JSON-Schema-driven fixture harness has no way to exercise (fixtures only drive JSON/JSON-Schema inputs through target-language outputs; there's no "TypeScript source as input" fixture pipeline in this repo). Added two unit tests to test/unit/typescript-input.test.ts (extending the existing precedent file for this input package):

  1. Asserts schemaForTypeScriptSources now attaches the correct qt-accessors map for a string enum.
  2. Drives the full pipeline — TypeScript source → schemaForTypeScriptSourcesJSONSchemaInputquicktype() with the TypeScript target (preferUnions: false) — and asserts the actual rendered output contains Foo = "000", Bar = "001", Baz = "002" and does not contain The000/The001/The002.

Both tests fail against the pre-fix code and pass after the fix.

Verification

  • npm run build passes.
  • npx vitest run test/unit/typescript-input.test.ts — 12/12 passed.
  • Re-ran the exact repro from the issue (--src enum.ts --lang typescript --just-types --no-prefer-unions) — output now correctly shows Foo, Bar, Baz.
  • Also manually verified the Swift case from the issue's follow-up comment (a long dotted string value) now produces case shortName instead of a mangled name derived from the value, confirming the fix is language-agnostic since it's applied at the schema level.
  • CI will run the full fixture suite across all target languages.

Fixes #1158

🤖 Generated with Claude Code

quicktype's TypeScript input converts .ts sources to JSON Schema via
typescript-json-schema, which drops each enum member's original
identifier and keeps only its runtime value. Downstream, enum case
names were synthesized from the value alone, so values that don't
look like valid identifiers (e.g. "000") produced mangled names like
The000 instead of the original member name Foo. This affected every
target language that renders named enum cases (TypeScript, Swift,
etc.), not just TypeScript output.

Walk the TypeScript program's EnumDeclaration nodes after schema
generation and attach a qt-accessors map (case value -> original
member name) to each string enum's schema definition. quicktype-core
already consumes qt-accessors as a per-case naming hint without
touching the serialized value, so no core changes were needed.

Added unit tests in test/unit/typescript-input.test.ts: one asserting
the qt-accessors map is generated correctly, and one driving the full
pipeline from TypeScript source to rendered TypeScript output to
confirm the original member names (not The000/The001/The002) appear
in the generated enum.

Fixes #1158

Co-Authored-By: gpt-5.6-sol via pi <noreply@openai.com>
@schani

schani commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

This typescript input thing is becoming a huge hack, plus it binds us to a specific TypeScript version. We should seriously investigate whether we can port this over to tree-sitter.

# Conflicts:
#	packages/quicktype-typescript-input/src/index.ts
#	test/unit/typescript-input.test.ts
@schani

schani commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

Agreed that the typescript-json-schema dependency and its pinned TypeScript version are a growing liability, and a tree-sitter-based front end is worth a serious look. That's a substantial rearchitecture of the whole quicktype-typescript-input package though, so I'd rather track it as its own issue than fold it into this targeted enum fix.

For what it's worth, this change tries hard not to deepen the coupling. It doesn't touch the schema-generation internals; it only walks the already-parsed program's EnumDeclaration nodes after the fact and reads member values through the stable public checker API (getSymbolAtLocation, getConstantValue), then writes the result into the existing qt-accessors hint that quicktype-core already understands. So the incremental version exposure here is minimal, and if/when the front end moves to tree-sitter this logic is small and self-contained enough to port along with the rest.

I'll open a separate issue for the tree-sitter investigation so it doesn't get lost.

@schani

schani commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

Filed as #3069 for the tree-sitter investigation.

…r names

The 'Kotlin: enum cases retain their TypeScript string values' test on
master asserted the pre-fix synthesized case name (Bringtofront, derived
from the value "bringtofront"). With this branch's fix, the original TS
member name BRING_TO_FRONT is preserved and rendered as BringToFront, so
update the assertion to the corrected output.

Co-Authored-By: Claude <noreply@anthropic.com>
@schani
schani merged commit 3e14899 into master Jul 21, 2026
28 checks passed
@schani
schani deleted the agent/fix-issue-1158 branch July 21, 2026 20:28
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.

Missing enum key from typescript string enums

1 participant