skip to content

Almost all TypeScript syntax disappears at compile time. What does `constructor(private id: string) {}` leave behind in the emitted JavaScript, and why does that matter for tools that only strip types?

level: middleimportance: should knowfreq 46%

answer

  1. most TypeScript syntax vanishes, this does not
  2. the constructor body gains a statement
  3. same club as enum and namespaces
  4. a stripper cannot invent that assignment
  5. erasableSyntaxOnly reports TS1294

basics

~20 s

A parameter property emits real code: the compiler generates this.id = id; in the constructor body, plus a bare field declaration when class fields use define semantics. That makes it one of the few TypeScript features a type-stripping tool cannot handle.

solid answer

~40 s

Erasing the annotation is not enough here — the member only exists because the compiler *generates* code for it. For `constructor(private id: string) {}` TypeScript emits `this.id = id;` as the first statement of the constructor body (after any `super(...)` call), and under define semantics for class fields it also emits a bare `id;` declaration in the class body. The `private` itself is compile-time only and vanishes. That puts parameter properties in the small club of TypeScript features with a runtime footprint, alongside `enum`, namespaces containing values, and legacy decorators. Tools that only delete type syntax cannot produce that assignment, which is why the compiler ships an `erasableSyntaxOnly` option that rejects the construct with error TS1294.

code

typescript · 6 lines
typescript
class Svc {
  constructor(private readonly id: string) {}
  get(): string { return this.id; }
}

console.log(new Svc("abc").get());

go deeper

for a junior

Know that the shorthand is not free syntax: the compiler writes an assignment into the constructor for you. Being able to sketch the emitted constructor is enough at this level.

for a middle

Be ready to write out the emitted JavaScript, including the extra bare field declaration under define semantics and the placement after super(...). Name the other non-erasable constructs.

for a senior

Explain the consequence for build pipelines: a codebase that wants type-stripping has to give the shorthand up, and erasableSyntaxOnly is how you enforce that at the compiler rather than discovering it at runtime.

for a principal

Own the policy decision. Weigh committing to erasable-only syntax across a codebase — what it forecloses, what it buys in tooling freedom, and how you would stage the migration without a flag-day rewrite.

## The rule this question is testing TypeScript's headline property is that the type layer is erased: annotations, `interface` declarations, generics, and `as` assertions all vanish and the output is the JavaScript you would have written. Parameter properties are one of the handful of exceptions, and an interviewer asking this is checking whether you know the difference between *syntax that is deleted* and *syntax that is compiled*. ## What the compiler actually emits Take the source: ```ts class Svc { constructor(private readonly id: string) {} get(): string { return this.id; } } ``` With class fields compiled to assignments (`useDefineForClassFields: false`, the default for targets below ES2022), the output is: ```js class Svc { constructor(id) { this.id = id; } get() { return this.id; } } ``` With define semantics (`useDefineForClassFields: true`, the default from target ES2022 upward), the compiler additionally emits the field declaration so the property is *defined* on the instance the way a real class field would be: ```js class Svc { id; constructor(id) { this.id = id; } get() { return this.id; } } ``` In a derived class the generated assignment is placed after the `super(...)` call, because touching `this` before `super` would throw. Notice what did **not** survive: the `string` annotation, and `private readonly`. Visibility and readonly-ness are checked by the compiler and then discarded — nothing in the output stops other code from reading or overwriting `svc.id`. ## The company it keeps The TypeScript features that emit runtime code rather than being erased are a short list worth memorizing: - **parameter properties**, which generate the constructor assignment; - **`enum`** (non-`const`), which generates a real object; - **namespaces that contain values**, which generate an IIFE and an object; - **legacy `experimentalDecorators`**, which generate decorator application calls. Everything else in the type layer is deleted. If you can recite that list, you can answer a whole family of questions about what TypeScript costs at runtime. ## Why type-stripping pipelines care A growing number of tools run TypeScript by *removing* type syntax rather than compiling it — the transformation is purely local, needs no type information, and preserves line positions so source maps stay trivial. Such a tool can delete `: string` from a parameter, but it cannot invent the `this.id = id;` statement, because that statement is not present in the source at all. A parameter property is therefore unsupportable by definition in that model. TypeScript's answer is a compiler option, `erasableSyntaxOnly`. With it on, the compiler reports every construct that would need generated code: ``` error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled. ``` pointing at the modifier on the parameter. The same error covers `enum` declarations, value-containing namespaces, and `import x = require(...)` aliases. Teams that intend to run their sources through a stripper turn the option on so the compiler, not the runtime, is the one that tells them. ## What you write instead The migration is mechanical and entirely local — declare the field and assign it: ```ts class Svc { private readonly id: string; constructor(id: string) { this.id = id; } } ``` Now every remaining piece of TypeScript in the class — the annotation, `private`, `readonly` — is erasable, and the field declaration plus the assignment are ordinary JavaScript that a stripper leaves alone. The class type is identical, so callers, subclasses, and `implements` checks are unaffected. ## The interview framing The strong answer has three beats. First, name the emitted artifact precisely: an assignment statement in the constructor, plus a field declaration under define semantics. Second, say *why* it cannot be erased — the member exists only because the compiler writes code that the author did not. Third, connect it to the practical consequence: this is what `erasableSyntaxOnly` exists to catch, and it is the reason a codebase committed to type-stripping has to give the shorthand up. A candidate who claims parameter properties are "just syntax sugar with no output" has missed the entire point of the question; sugar that generates a statement is still generated code. ## One thing to be careful about Do not overclaim about visibility. `private` on a parameter property is exactly as compile-time-only as `private` on a normal member — the generated assignment is a plain `this.id = id`, with no runtime check, no closure, and no hidden storage. If you want runtime privacy you need a JavaScript `#` field, which cannot be written as a parameter property at all.

  • Which other TypeScript constructs are non-erasable in the same way?
    Non-`const` `enum` declarations, which emit a real object; namespaces that contain values, which emit an IIFE; and legacy `experimentalDecorators`, which emit decorator application calls. Parameter properties join them because each one produces JavaScript the author never wrote. Everything else in the type layer — annotations, `interface`, `type`, generics, `as` — is deleted outright.
  • How would you rewrite a class full of parameter properties so it becomes erasable?
    Declare each field explicitly in the class body and assign it in the constructor: `private readonly id: string;` plus `this.id = id;`. The transformation is purely local and leaves the class type untouched, so callers and subclasses are unaffected. Turn on `erasableSyntaxOnly` afterwards so the compiler flags any parameter property that creeps back in.
  • Does the emitted `private` do anything at runtime?
    No. `private` is checked by the compiler and then discarded; the emitted line is a plain `this.id = id`, so the property is an ordinary own property that any JavaScript code can read or overwrite. Runtime privacy requires a JavaScript `#` field, and `#` names cannot be declared as parameter properties — they need an explicit field declaration.

saying these in an interview costs you the question

  • Says parameter properties are erased like every other TypeScript syntax
  • Thinks the emitted private adds a runtime access check
  • Believes the generated member lands on the prototype
  • Assumes any type-stripping tool can run the class unchanged
  • Confuses the generated assignment with a decorator's emit

context