fix(transformer/legacy-decorator): return runtime binding for design:type#22640
Conversation
How to use the Graphite Merge QueueAdd either label to this PR to merge it via the merge queue:
You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has enabled the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. This stack of pull requests is managed by Graphite. Learn more about stacking. |
Merging this PR will not alter performance
Comparing Footnotes
|
d70377f to
7e37f2c
Compare
Merge activity
|
…type (#22640) Closes #22227. Closes #22228. ## Summary - Switch `design:type` / `design:paramtypes` / `design:returntype` emit from `typeof X === "function" ? X : Object` to `typeof X === "undefined" ? Object : X`, matching SWC and Babel. Cross-file imported enums and namespace-imported enums (`NS.E`) now return the actual enum object instead of silently degrading to `Object` — consumers like type-graphql, typeorm, NestJS Swagger, AutoMapper, and class-validator can introspect via `Object.values()`. - Cases where the runtime binding is genuinely missing still fall back to `Object` (stale ambient declarations, erased type imports, unresolved references), so existing behavior is preserved everywhere else. - See #14740 for the design rationale: matching SWC/Babel rather than tsc's primitive-type emit (which requires cross-file type information OXC doesn't have). - Conformance: 5 existing fixtures updated for the new emit shape, 6 new fixtures cover the bug paths and predicate boundaries; no `Output mismatch` regressions. ## Example Input: ```ts import { StringEnum } from './enums'; class Source { @dec value!: StringEnum; } ``` Before: ```js // design:type === Object — StringEnum is an enum object, typeof !== "function" babelHelpers.decorateMetadata("design:type", typeof (_ref = typeof StringEnum !== "undefined" && StringEnum) === "function" ? _ref : Object) ``` After: ```js // design:type === StringEnum (the enum object — introspectable via Object.values) babelHelpers.decorateMetadata("design:type", typeof StringEnum === "undefined" ? Object : StringEnum) ``` ## Note on the SWC enum-metadata debate [swc-project/swc#11033](swc-project/swc#11033) argues SWC's emit is wrong for cross-file enums — the runtime returns the enum object (an `{...}`) rather than `Number` / `String` / `Object` as tsc would. This PR adopts the same emit shape SWC and Babel use, so it has the same property: cross-file enum metadata is the enum binding itself, not a primitive constructor. OXC chose this side of the debate deliberately in #14740 — returning the enum object is what type-graphql, typeorm, NestJS Swagger, AutoMapper, and class-validator depend on to introspect enum members. Matching tsc's primitive emit would require cross-file type information OXC doesn't have, and would unblock no real consumer. If TypeScript publishes a position on what non-type-checker transformers should emit, this can be revisited. AI disclosure: drafted with Claude Code, reviewed manually.
7e37f2c to
e421ef0
Compare
There was a problem hiding this comment.
I'd recommend adding an option that is disabled by default as this is also treated as a bug in the babel plugin (leonardfactory/babel-plugin-transform-typescript-metadata#11).
(so that we align with typescript by default)
This was in mind, actually. I chose to align back to SWC and Babel as they have already worked well for a long while and got a bare identifier actually good as they could use it to check and do something special they want. I just looked at the issue you mentioned; I think even the original implementation could produce a wrong type. In that issue example, it should be a It is a trade-off, but I am not opposed to adding an option for this if it proves to be very useful. I am happy to do it. |
|
IMO the behavior is inconsistent and confusing. That said, the decorator metadata will be fading out anyways (microsoft/TypeScript#57533 (comment)), so my opinion is not so strong. |
|
I guess discouraging the use of the feature + explaining the inconsistent behavior at https://oxc.rs/docs/guide/usage/transformer/typescript.html#decorators would help the users. |
### 🚀 Features - e857b0c napi/minify: Expose legalComments option and result (#20370) (Boshen) - 661132d parser: More friendly error messages for rest assignment target and rest binding element (#22719) (sapphi-red) - ee659b6 transformer/legacy-decorator: Add `strictNullChecks` option for nullable-union design:type (#22266) (Kyle Cannon) ### 🐛 Bug Fixes - e1d064e transformer/class-properties: Reparent lifted private method helpers (#22716) (Cameron) - 4ac0fca minifier: Preserve `0 && (module.exports = { ... })` cjs-module-lexer hint (#22729) (Dunqing) - 40ff611 minifier: Mark peephole loop changed when dropping dead-after-throw statement (#22722) (Dunqing) - 2f7b210 codegen: Emit pife-arrow/function leading comments inside the wrap (#22720) (Dunqing) - e184f74 parser: Improve invalid `import` property access diagnostic (#22693) (camc314) - 7baed9c transformer/private-method: Clear inherited strict flags (#22508) (camc314) - a9ad27e parser: Keep annotation comments leading without preceding newline (#22711) (Dunqing) - 9ea4d64 minifier: Re-evaluate pure/no-side-effects flags after peephole inlining (#22595) (Dunqing) - 07afbb6 minifier: Drop empty-body IIFE wrapper when called with arguments (#22589) (Dunqing) - fa7c463 semantic: Correct TS enum member symbol spans (#22689) (camc314) - 26b9396 semantic: Resolve parameter decorators outside parameter scope (#22623) (camc314) - b284045 parser: Switch to module goal eagerly on `export` (#22684) (Boshen) - dfa931d semantic: Propagate unresolved auto-increment enum value instead of defaulting to 0 (#22646) (Dunqing) - 69a6ba6 transformer/legacy-decorator: Emit Array for ReadonlyArray<T> in decorator metadata (#22265) (Kyle Cannon) - e421ef0 transformer/legacy-decorator: Return runtime binding for design:type (#22640) (Dunqing) - d61e1d7 codegen: Preserve verbatim text of pure/no-side-effects comments (#22525) (Dunqing) - 702b14e minifier: Preserve IIFE structure in DCE-only mode (#22547) (Dunqing) - 917da24 parser: Apply PURE comment through member-access chains (#22566) (Dunqing) - a069b1c codegen: Preserve quotes for cjs-module-lexer equality strings (#22551) (Dunqing) ### ⚡ Performance - 2f623b0 semantic: Skip unresolved checks for re-exports (#22660) (camc314) - 0d9553d semantic: Early-exit `check_object_expression` for objects with <2 properties (#22668) (Dunqing) - d721ad9 semantic: Use direct grandparent lookup for TS type parameters (#22658) (camc314) - 0aff288 semantic: Reorder numeric literal strict mode checks (#22657) (camc314) - 4d5ddb1 semantic: Reorder binding identifier checks (#22656) (camc314) - e32acd8 semantic: Reorder identifier ambient binding check (#22653) (camc314) - 09fe178 semantic: Reorder ident reference strict mode check (#22652) (camc314) - 4b6add2 semantic: Avoid duplicate ident clone for bindings (#22663) (camc314) - 82f9662 parser: Check identifier kind before context flag (#22662) (camc314) - d7cd951 parser: Fast path identifier parsing and inline operator helpers (#22650) (Boshen) - 7b84314 semantic: Use direct byte access for numeric leading-zero check (#22642) (camc314) - 0345a31 semantic: Pre-size class elements hash map (#22618) (camc314) - 04d3065 minifier: Drop per-call buffers in try_fold_concat (#22596) (Dunqing) - 4f289f1 semantic: Resolve_references_for_current_scope without a temp Vec (#22599) (Dunqing) - e862c15 semantic: Avoid heap alloc for var hoist scope ids (#22603) (Dunqing) - 8ff8674 semantic: Early return if `excess` is `0` in `Stats::increase_by` (#22616) (camc314) - 7a4120e semantic: Pre-reserve unresolved_references using Stats::references (#22580) (Dunqing) Co-authored-by: Dunqing <[email protected]>

Closes #22227.
Closes #22228.
Summary
design:type/design:paramtypes/design:returntypeemit fromtypeof X === "function" ? X : Objecttotypeof X === "undefined" ? Object : X, matching SWC and Babel. Cross-file imported enums and namespace-imported enums (NS.E) now return the actual enum object instead of silently degrading toObject— consumers like type-graphql, typeorm, NestJS Swagger, AutoMapper, and class-validator can introspect viaObject.values().Object(stale ambient declarations, erased type imports, unresolved references), so existing behavior is preserved everywhere else.Output mismatchregressions.Example
Input:
Before:
After:
Note on the SWC enum-metadata debate
swc-project/swc#11033 argues SWC's emit is wrong for cross-file enums — the runtime returns the enum object (an
{...}) rather thanNumber/String/Objectas tsc would. This PR adopts the same emit shape SWC and Babel use, so it has the same property: cross-file enum metadata is the enum binding itself, not a primitive constructor.OXC chose this side of the debate deliberately in #14740 — returning the enum object is what type-graphql, typeorm, NestJS Swagger, AutoMapper, and class-validator depend on to introspect enum members. Matching tsc's primitive emit would require cross-file type information OXC doesn't have, and would unblock no real consumer. If TypeScript publishes a position on what non-type-checker transformers should emit, this can be revisited.
AI disclosure: drafted with Claude Code, reviewed manually.