Repository navigation
JSDoc @private / @protected are dropped in declaration emit for properties declared by constructor assignment #64628
Description
Version: typescript@7.0.2 (tsgo) · Area: Declaration Emit / JavaScript + JSDoc
Summary
When a .js file declares a class property the classical JSDoc way — a JSDoc-annotated assignment inside
the constructor — Corsa emits the member without its visibility modifier, and for @private also
without its type:
// a.js
export class A {
constructor() {
/** @private @type {boolean} */
this.p1 = false;
/** @protected @type {string} */
this.p3 = '';
}
}| compiler | emitted a.d.ts |
|---|---|
| 4.7.4, 4.9.5, 5.9.3, 6.0.3 | /** @private @type {boolean} */private p1;/** @protected @type {string} */protected p3: string; |
| 7.0.2 | /** @private @type {boolean} */p1;/** @protected @type {string} */p3: string; |
The member therefore becomes public in the published types, and a @private one additionally becomes
untyped, which turns into error TS7008: Member 'p1' implicitly has an 'any' type for every consumer that
does not set skipLibCheck: true.
Note the sibling form is emitted correctly — a property declared in the class body keeps the modifier:
export class B {
/** @private @type {boolean} */
f1 = false;
}→ private f1; (both Strada and Corsa). Only the constructor-assignment form regresses.
Reproduction
tsconfig.json
{
"compilerOptions": {
"target": "ESNext", "module": "NodeNext", "moduleResolution": "NodeNext",
"allowJs": true, "checkJs": true, "strict": true,
"declaration": true, "emitDeclarationOnly": true, "outDir": "out",
"skipLibCheck": true, "lib": ["es2017", "dom"]
},
"include": ["a.js"]
}npx tsgo -p tsconfig.json && cat out/a.d.tsMinimum repro is the A class above. Declaring it in the class body (B) is unaffected.
Real-world hit: @lion/ui (ING's design system)
@lion/ui is a ~900-file JSDoc-typed .js library that ships dist-types/*.d.ts. Everything is authored in
this style, so the regression affects 64 members in 21 files (measured by building the same commit with
typescript@4.9.5 and with 7.0.2 and comparing the emitted members); 58 of them can be worked around in
source, the other six cannot (see Notes).
Concrete reference on the current default branch (commit 949d32402 of
https://github.com/ing-bank/lion — file packages/ui/components/calendar/src/LionCalendar.js, around lines
190-191):
/** @private */
this.__today = normalizeDateTime(new Date());- Strada (the same commit built with
typescript@4.9.5, i.e. what the package published until the
TypeScript 7 move) emits, indist-types/components/calendar/src/LionCalendar.d.ts:/** @private */ private __today; - Corsa 7.0.2 (that commit's actual output) emits:
/** @private */ __today;
Measured over the whole package (same sources, same consumer):
emit of @lion/ui |
consumer errors at skipLibCheck: false |
|---|---|
typescript@4.9.5 output |
25, of which 0 × TS7008 |
7.0.2 output |
100, of which 74 × TS7008 |
The 74 break down as 42 unique members across 16 files, e.g. LionCalendar (__today,
__connectedCallbackDone, __eventsAdded, __bound*Delegation …), OverlayController (7 members),
LionCombobox (5), OverlaysManager (4). The @protected half is worse than noise: protected _ariaVersion
becomes public _ariaVersion, and several downstream TS2611/TS2416/TS2417 errors follow from
protected members turning public.
Expected behaviour
For the constructor-assignment form, Corsa should emit what Strada emits: the visibility modifier
(private / protected, and presumably @readonly/@public likewise) and, for @private, keep dropping the
type exactly as Strada does — i.e. private p1;.
Notes
declare p1;is not an option for us in.js(TS8009: The 'declare' modifier can only be used in TypeScript files), so there is no source-level way to ask for the Strada output. An interface or
@typedefthat redeclares the member is inert too - an interface cannot narrow a class member's
visibility and it does not influence what is emitted for it. The only workaround is to add a
class-body field declaration carrying the JSDoc, which is not semantics-preserving (it changes
initialization order / property-existence, and for Lit reactive properties it shadows the accessor Lit
installs on the prototype), so we cannot take it for all members. We took it for 58 of the 64 members;
the remaining six are Lit reactive properties.- The native-port staging repo (
microsoft/typescript-go, now closed and pointing here) documented that declaration emit for.jsinput "has substantially changed behavior" and asked for issues where the.d.tsoutput from a.jsfile is incorrect - this looks like one.mdsays declaration emit for.jsinput "has substantially changed behavior" and asks for issues where the.d.tsoutput from a.js` file is incorrect — this looks like one. - Related, but different: Issues exporting a single d.ts for our code base #4315 (modifier order for
@privatestatic methods), JSDoc@privateon constructor without parameters is omitted in declaration files #58653 (@privateon a
parameterless constructor), and Fix line endings in program.ts #3541, which targets the samethis.xcollection in JS declaration emit - the code path this report exercises.
Activity
belentani7 commentedon Oct 5, 2026
This is a declaration emit bug that affects JSDoc visibility annotations on constructor-assigned properties.
Impact:
When you declare a property via constructor parameter assignment (TypeScript's parameter properties), the Kristoffer Langeland Knudsen (@Private) and Protected JSDoc tags are not preserved in the generated .d.ts files. This breaks encapsulation documentation for consumers.
Example:
\\ ypescript
class Foo {
/** Kristoffer Langeland Knudsen (@Private) */
constructor(private bar: string) {} // Kristoffer Langeland Knudsen (@Private) lost in .d.ts
}
\\
Expected: The .d.ts should include /** Kristoffer Langeland Knudsen (@Private) */ bar: string;
Fix location: Likely in the declaration emit transformer where parameter properties are converted to property declarations. The JSDoc extraction needs to run before the parameter property transformation.
Workaround: Declare properties explicitly in the class body instead of using parameter properties.
Want me to investigate the exact code path in the compiler?
tlouisse commentedon Oct 5, 2026
Yes, it would be great if we can keep using this jsdoc annotation without having to do workarounds