Skip to content

fix: strip uninitialized class fields (useDefineForClassFields: false)#206

Open
ashley-hunter wants to merge 4 commits intovoidzero-dev:mainfrom
ashley-hunter:fix/strip-uninitialized-class-fields
Open

fix: strip uninitialized class fields (useDefineForClassFields: false)#206
ashley-hunter wants to merge 4 commits intovoidzero-dev:mainfrom
ashley-hunter:fix/strip-uninitialized-class-fields

Conversation

@ashley-hunter
Copy link
Copy Markdown
Contributor

@ashley-hunter ashley-hunter commented Apr 2, 2026

Summary

  • Strip uninitialized class fields during compilation, matching TypeScript's useDefineForClassFields: false behavior (Angular's default)
  • Fixes runtime crashes where _defineProperty(this, "field", void 0) shadows prototype getters set up by legacy decorators (@Select, @Dispatch)
  • AOT path: scans all classes for PropertyDefinition nodes without initializers and deletes them via text edits
  • JIT path: enables oxc_transformer's existing remove_class_fields_without_initializer option
  • New strip_uninitialized_fields option (default: true) on TransformOptions
  • Preserves JavaScript private fields (#foo) — these are runtime syntax, not TypeScript type annotations
  • Emits __decorate() calls for non-Angular decorators on stripped fields (e.g., @Select, @Dispatch), matching tsc's output where the field is removed from the class body but the decorator is applied via __decorate([...], Class.prototype, "field", void 0)
  • Imports __decorate from tslib when decorator calls are emitted

Possibly Closes #73

Field stripping rules

Field Has Initializer? Has Decorator? Private (#)? Action
foo = 'bar' Yes No No Keep
@Dispatch() action = () => ... Yes Yes No Keep (decorator handled by existing logic)
@Select(...) field$: Observable<T> No Yes No Strip field, emit __decorate() call
field: Observable<T> No No No Strip
#private: string No No Yes Keep (JS runtime syntax)
static field: string No No No Keep (static fields follow different rules)

Test plan

  • 11 integration tests covering: basic stripping, non-Angular classes, static field preservation, declare fields, initializer preservation, opt-out, decorated fields with __decorate emission, private field preservation, JIT mode
  • All existing tests pass (3 snapshots updated for correct new behavior)
  • Manual verification: linked locally in a project with @Select/@Dispatch decorators

🤖 Generated with Claude Code

When `useDefineForClassFields: false` (Angular's default), TypeScript
treats uninitialized fields as type-only declarations that produce no JS
output. The compiler was preserving these fields, causing `vite:oxc` to
lower them to `_defineProperty(this, "field", void 0)` which shadows
prototype getters set up by legacy decorators (@select, @dispatch).

AOT path: scan all classes for PropertyDefinition nodes without
initializers and emit Edit::delete() spans for them.

JIT path: enable oxc_transformer's existing
`remove_class_fields_without_initializer` option in strip_typescript().

Controlled by new `strip_uninitialized_fields` option (default: true).

Closes voidzero-dev#73

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ashley-hunter ashley-hunter force-pushed the fix/strip-uninitialized-class-fields branch from 0c3d78c to 2078247 Compare April 2, 2026 12:53
@ashley-hunter ashley-hunter marked this pull request as draft April 2, 2026 13:26
ashley-hunter and others added 2 commits April 2, 2026 15:21
Private fields (#foo) are JavaScript runtime syntax that declares a
private slot on the class. They must not be stripped even without an
initializer, unlike public fields which are TypeScript type annotations
under useDefineForClassFields: false. Stripping them causes rolldown
to panic because this.#foo references remain but the declaration is gone.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When an uninitialized field has a non-Angular decorator (@select,
@dispatch, etc.), the field declaration is stripped but the decorator
must survive as a __decorate() call on the prototype. This matches
tsc's output with useDefineForClassFields: false — the field is gone
from the class body but the decorator is applied via __decorate().

Without this, stripping the field also removes the decorator, so the
prototype getter is never set up and the property is undefined at
runtime.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ashley-hunter ashley-hunter marked this pull request as ready for review April 2, 2026 14:55
Recent commits introduced unformatted Rust code in transform.rs and
integration_test.rs, causing the `cargo fmt --check` CI step to fail.

https://claude.ai/code/session_01DWNfhcEuAQRhSXyzv3abY6
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.

Support useDefineForClassFields: false (Class Field Lowering)

2 participants