Skip to content

Fix false-positive TS2354 for native private class field access with importHelpers at dated targets - #63729

Closed
Andrew Stegmaier (astegmaier) wants to merge 1 commit into
microsoft:mainfrom
astegmaier:fix-63728-private-field-tslib-false-positive
Closed

Fix false-positive TS2354 for native private class field access with importHelpers at dated targets#63729
Andrew Stegmaier (astegmaier) wants to merge 1 commit into
microsoft:mainfrom
astegmaier:fix-63728-private-field-tslib-false-positive

Conversation

@astegmaier

Copy link
Copy Markdown
Contributor

Fixes #63728.

The bug

With importHelpers: true and a dated target (e.g. ES2022ES2025), tsc incorrectly reports TS2354: This syntax requires an imported helper but module 'tslib' cannot be found for plain, fully-native private field/method/accessor access (this.#x, #x in obj, static private fields/methods, private accessors, static blocks, private fields in generics/closures/class expressions/inheritance) — even though the emitted JS for all of these is 100% native ES2022+ syntax that never references tslib.

Root cause

checkPropertyAccessExpressionOrQualifiedName, checkInExpression, and setNodeLinksForPrivateIdentifierScope gate the private-identifier helper-requirement check on:

languageVersion < LanguageFeatureMinimumTarget.PrivateNamesAndClassStaticBlocks ||
languageVersion < LanguageFeatureMinimumTarget.ClassAndClassElementDecorators ||
!useDefineForClassFields
  • ClassAndClassElementDecorators is pinned to the ESNext sentinel because TC39 decorators have never been assigned to a dated ECMAScript edition. That makes languageVersion < ClassAndClassElementDecorators true for every dated target, forever — regardless of whether the file uses decorators at all. The actual emitter (classFields.ts's shouldTransformPrivateElementsOrClassStaticBlocks) only checks languageVersion < ES2022, with no decorator-related gating, so this term in the checker never corresponded to real emit behavior.
  • !useDefineForClassFields has the same problem: private fields are always emitted using "define" semantics regardless of useDefineForClassFields (that flag only affects public fields), so it also doesn't correspond to any real difference in whether tslib is needed for private-field access. Verified by emitting with --useDefineForClassFields false --target es2022: output is fully native with no tslib references, yet the checker still errored before this fix.

The fix

Removes both spurious clauses from the 3 call sites above, leaving just languageVersion < LanguageFeatureMinimumTarget.PrivateNamesAndClassStaticBlocks.

A 4th call site with the same ClassAndClassElementDecorators check, inside getFirstTransformableStaticClassElement, is intentionally left unchanged. That one only matters when a class is decorated, to decide if the decorator transform's hoisting of static private/static-block elements into an IIFE needs the __setFunctionName helper. I verified (by emitting @dec class C { static #foo() {} } at ES2022 with --outDir) that this really is required in the actual output, so removing this clause caused a real regression in esDecorators-classDeclaration-missingEmitHelpers-classDecorator.3.ts; that edit was reverted.

Testing

  • Added tests/cases/compiler/importHelpersNoHelpersForPrivateFieldsAtES2022.ts — target ES2022, importHelpers: true, no tslib present, covering instance/static private fields, private methods, static private methods, private accessors, private auto-accessors (instance + static), static blocks, and #x in obj. Asserts zero errors; baseline .js confirms the emit never references tslib.
  • Added tests/cases/compiler/importHelpersNoHelpersForPrivateFieldsAtES2022UseDefineForClassFieldsFalse.ts — same coverage with useDefineForClassFields: false, confirming that flag no longer triggers the false positive either.
  • Ran the full test suite (hereby runtests-parallel --light=false, 106,383 tests) — all passing, no baseline regressions.
  • hereby lint passes.
  • Manually confirmed decorators and using declarations still correctly require tslib at dated targets (these are real transforms, not part of this bug).
  • Manually reproduced and confirmed the fix against all 13 "confirmed bug" cases and both control cases from the original repro (https://github.com/astegmaier/typescript-private-field-tslib-repro), and confirmed the 4 "correctly required" cases (decorators, using) are unaffected.

…ted targets

checkPropertyAccessExpressionOrQualifiedName, checkInExpression, and
setNodeLinksForPrivateIdentifierScope gated the `tslib` helper
requirement for private-identifier access on
`languageVersion < LanguageFeatureMinimumTarget.ClassAndClassElementDecorators`
in addition to the private-fields-specific threshold. Since decorators
have never been assigned a dated ECMAScript edition,
ClassAndClassElementDecorators is pinned to the ESNext sentinel, so
this condition was true for every dated target (ES2022 through
ES2025) unconditionally - regardless of whether the file used
decorators at all. This caused `tsc` to require `tslib` (and hard
error with TS2354 if it couldn't be resolved) for plain, fully-native
private field/method/accessor access, even though the actual emitted
JS never references tslib for these constructs (the emitter's
shouldTransformPrivateElementsOrClassStaticBlocks in classFields.ts
only checks languageVersion < ES2022).

The `!useDefineForClassFields` clause on the same three checks had the
same problem: private fields are always emitted using 'define'
semantics regardless of useDefineForClassFields (which only affects
public fields), so it doesn't correspond to any real difference in
whether tslib is needed for private-field access.

The fourth call site with the same ClassAndClassElementDecorators
check, inside getFirstTransformableStaticClassElement, is left
unchanged: it's used only to decide whether a *decorated* class needs
the __setFunctionName helper for hoisting its static private/static-block
elements, which is a genuine, verified coupling between decorators and
private statics.

Fixes microsoft#63728

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@astegmaier

Copy link
Copy Markdown
Contributor Author

I'm closing this PR per Ryan Cavanaugh (@RyanCavanaugh)'s advice that the only thing that's appropriate to go into the 6.0 branch is security fixes - and this is definitely not that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

For Uncommitted Bug PR for untriaged, rejected, closed or missing bug

Projects

None yet

1 participant