🔎 Search Terms
TS2354, "requires an imported helper", tslib private field, importHelpers ES2022, classPrivateFieldGet ES2022, classPrivateFieldSet ES2022, classPrivateFieldIn, ClassAndClassElementDecorators, LanguageFeatureMinimumTarget, private field tslib not needed, importHelpers private field false positive, private field decorator target ESNext
🕗 Version & Regression Information
- This is the behavior in every version I tried, and I reviewed the FAQ (no entries about
tslib, importHelpers, or private fields exist there).
- Reproduces on every TypeScript version from 4.3.5 (when native
#private fields and the ES2022 target were introduced) through the current 7.0.2 and the 7.1.0-dev nightly — i.e. every version able to target ES2022.
- Not a regression introduced at any specific version, but the mechanism changed once, without changing the observable behavior: TS 4.3.5 through 5.4.5 used a blunt, unconditional
if (languageVersion < ScriptTarget.ESNext) guard around the private-field helper check. TS 5.5.2 refactored this into today's three-term LanguageFeatureMinimumTarget-based condition (see "Additional information" below) — but for private fields specifically, the observable result is identical in every version: every dated target requires tslib; only the literal string "ESNext" doesn't.
- I was unable to reproduce this in the Playground specifically (see below), so I could not bisect via the Playground's version picker; I instead bisected locally by installing each
typescript@x.y.z from npm and running tsc directly (versions tested: 4.3.5, 4.5.5, 4.7.4, 4.9.5, 5.0.4, 5.1.6, 5.2.2, 5.3.3, 5.4.5, 5.5.4, 5.6.3, 5.7.3, 5.8.3, 5.9.3, 6.0.3, 7.0.2, 7.1.0-dev — all reproduce it identically).
⏯ Playground Link
https://github.com/astegmaier/typescript-private-field-tslib-repro (you have to do this locally because the playground always has tslib installed)
💻 Code
// tsconfig.json: { "target": "ES2022", "importHelpers": true }
// no tslib installed in node_modules
export class C {
#x = 0;
get(): number { return this.#x; } // needs __classPrivateFieldGet per tsc
set(v: number): void { this.#x = v; } // needs __classPrivateFieldSet per tsc
has(o: unknown): boolean { return #x in (o as C); } // needs __classPrivateFieldIn per tsc
}
🙁 Actual behavior
With target: "ES2022" (or any dated target through "ES2025") and importHelpers: true, and no tslib installed, tsc reports:
error TS2354: This syntax requires an imported helper but module 'tslib' cannot be found.
on the very first private-field access it checks (get(), above — commenting that method out reveals set() fails identically, and commenting that out too reveals has() fails identically; tsc only ever reports the first one per file). This happens for every form of native #private member access I could find: field reads/writes, compound assignment, optional chaining, brand checks (#x in obj), private methods, private accessors, static private fields/methods, static blocks, and private fields inside class expressions, generic classes, subclasses, and closures.
Once tslib is installed, the error goes away — but the compiled output for every one of the constructs above contains zero references to tslib, at any target. I confirmed this by forcing full (non-declaration-only) emission with tslib present and installed, and grepping the output .js for tslib: no matches, for any of the 13 private-field-related constructs listed above, at ES2022 through ES2025.
🙂 Expected behavior
tsc should not require tslib to be resolvable for constructs that are natively supported at the configured target and that the emitter never actually references, the same way it doesn't for e.g. arrow functions or let/const at ES2022.
Additional information about the issue
Root cause. In checker.ts's checkPropertyAccessExpressionOrQualifiedName (and the parallel check in checkInExpression for #x in obj), the private-field helper requirement is gated by:
if (isPrivateIdentifier(right)) {
if (
languageVersion < LanguageFeatureMinimumTarget.PrivateNamesAndClassStaticBlocks ||
languageVersion < LanguageFeatureMinimumTarget.ClassAndClassElementDecorators ||
!useDefineForClassFields
) {
// ...checkExternalEmitHelpers(..., ClassPrivateFieldGet / ClassPrivateFieldSet)
}
and in types.ts:
export const LanguageFeatureMinimumTarget: Record<LanugageFeatures, ScriptTarget> = {
PrivateNamesAndClassStaticBlocks: ScriptTarget.ES2022, // correct: native since ES2022
ClassAndClassElementDecorators: ScriptTarget.ESNext, // <- pinned to the ESNext sentinel forever
...
};
ClassAndClassElementDecorators is pinned to ScriptTarget.ESNext (99) because TC39 decorators have never been assigned to a dated ECMAScript edition. So languageVersion < LanguageFeatureMinimumTarget.ClassAndClassElementDecorators is true for every dated target — making the whole || condition unconditionally true regardless of target, regardless of whether the file uses decorators at all, and regardless of useDefineForClassFields. Only the literal target string "ESNext" clears that term. This appears to be an unintentional side effect of gating an unrelated feature's (decorators') native-support threshold together with private fields' (correctly-gated-at-ES2022) one, rather than a deliberate design choice.
🔎 Search Terms
TS2354, "requires an imported helper", tslib private field, importHelpers ES2022, classPrivateFieldGet ES2022, classPrivateFieldSet ES2022, classPrivateFieldIn, ClassAndClassElementDecorators, LanguageFeatureMinimumTarget, private field tslib not needed, importHelpers private field false positive, private field decorator target ESNext
🕗 Version & Regression Information
tslib,importHelpers, or private fields exist there).#privatefields and theES2022target were introduced) through the current 7.0.2 and the 7.1.0-dev nightly — i.e. every version able to targetES2022.if (languageVersion < ScriptTarget.ESNext)guard around the private-field helper check. TS 5.5.2 refactored this into today's three-termLanguageFeatureMinimumTarget-based condition (see "Additional information" below) — but for private fields specifically, the observable result is identical in every version: every datedtargetrequirestslib; only the literal string"ESNext"doesn't.typescript@x.y.zfrom npm and runningtscdirectly (versions tested: 4.3.5, 4.5.5, 4.7.4, 4.9.5, 5.0.4, 5.1.6, 5.2.2, 5.3.3, 5.4.5, 5.5.4, 5.6.3, 5.7.3, 5.8.3, 5.9.3, 6.0.3, 7.0.2, 7.1.0-dev — all reproduce it identically).⏯ Playground Link
https://github.com/astegmaier/typescript-private-field-tslib-repro (you have to do this locally because the playground always has tslib installed)
💻 Code
🙁 Actual behavior
With
target: "ES2022"(or any dated target through"ES2025") andimportHelpers: true, and notslibinstalled,tscreports:on the very first private-field access it checks (
get(), above — commenting that method out revealsset()fails identically, and commenting that out too revealshas()fails identically;tsconly ever reports the first one per file). This happens for every form of native#privatemember access I could find: field reads/writes, compound assignment, optional chaining, brand checks (#x in obj), private methods, private accessors, static private fields/methods, static blocks, and private fields inside class expressions, generic classes, subclasses, and closures.Once
tslibis installed, the error goes away — but the compiled output for every one of the constructs above contains zero references totslib, at any target. I confirmed this by forcing full (non-declaration-only) emission withtslibpresent and installed, and grepping the output.jsfortslib: no matches, for any of the 13 private-field-related constructs listed above, atES2022throughES2025.🙂 Expected behavior
tscshould not requiretslibto be resolvable for constructs that are natively supported at the configuredtargetand that the emitter never actually references, the same way it doesn't for e.g. arrow functions orlet/constatES2022.Additional information about the issue
Root cause. In
checker.ts'scheckPropertyAccessExpressionOrQualifiedName(and the parallel check incheckInExpressionfor#x in obj), the private-field helper requirement is gated by:and in
types.ts:ClassAndClassElementDecoratorsis pinned toScriptTarget.ESNext(99) because TC39 decorators have never been assigned to a dated ECMAScript edition. SolanguageVersion < LanguageFeatureMinimumTarget.ClassAndClassElementDecoratorsistruefor every dated target — making the whole||condition unconditionally true regardless oftarget, regardless of whether the file uses decorators at all, and regardless ofuseDefineForClassFields. Only the literal target string"ESNext"clears that term. This appears to be an unintentional side effect of gating an unrelated feature's (decorators') native-support threshold together with private fields' (correctly-gated-at-ES2022) one, rather than a deliberate design choice.