skip to content

Node can run a .ts file by stripping its type syntax rather than compiling it. Which TypeScript constructs cannot be handled by stripping alone, and which tsconfig flag makes the compiler report them?

level: middleimportance: should knowfreq 40%

answer

  1. most syntax is annotation and vanishes
  2. a few features emit real objects
  3. enum, namespace, parameter properties
  4. one flag forbids exactly those
  5. erasable-only is the option name

basics

~10 s

Constructs that emit runtime code cannot simply be erased: enum, namespaces containing runtime members, constructor parameter properties, and import x = require(...). The tsconfig flag erasableSyntaxOnly makes the TypeScript compiler report exactly those.

solid answer

~40 s

Most TypeScript syntax is pure annotation, so a runtime can execute a `.ts` file by blanking the type syntax out — that is what Node's `--experimental-strip-types` does, introduced in Node 22.6 and enabled by default in later releases. But a few TypeScript features are not annotations at all: `enum` generates a real object, a `namespace` with runtime members generates a function and an object, a constructor **parameter property** like `constructor(private x: number)` generates an assignment, and `import x = require(...)` / `export =` generate module plumbing. Deleting those changes behaviour rather than preserving it, so a stripping-only runtime rejects them; you need a transforming pipeline instead. TypeScript 5.8 added `erasableSyntaxOnly`, which makes the compiler flag exactly this syntax so the codebase stays runnable by any strip-only tool.

code

typescript · 19 lines
typescript
// Not erasable: each of these emits runtime code.
enum Color { Red, Green }

class Point {
  constructor(private x: number) {}
}

// Erasable replacements.
const COLOR = { Red: 0, Green: 1 } as const;
type ColorValue = (typeof COLOR)[keyof typeof COLOR];

class ErasablePoint {
  private x: number;
  constructor(x: number) {
    this.x = x;
  }
}

console.log(Color.Red, new Point(1), COLOR.Green, new ErasablePoint(2));

go deeper

for a junior

Remember the headline: nearly all TypeScript syntax disappears at runtime, but enum is a real object that exists after compilation. Be able to give that one example when asked what survives erasure.

for a middle

List the non-erasable constructs and explain why deleting each one changes behaviour, then name erasableSyntaxOnly as the compiler option that reports them. Say plainly that stripping is not checking.

for a senior

Show the architectural consequence: adopting a strip-only runner constrains what the codebase may use, and frameworks built on parameter properties or decorator injection are ruled out. Explain the erasable rewrites you would standardise on.

for a principal

Frame it as portability of the toolchain — keeping the source erasable means no single build tool is load-bearing. Be ready to weigh that freedom against the cost of banning idioms a team or framework already depends on.

## Erasure is the normal case The organising rule of TypeScript is that types are erased: the emitted JavaScript is the input with the type layer removed. Annotations, `interface` declarations, `type` aliases, generic parameters, `as` assertions, `satisfies`, non-null `!`, and `implements` clauses all vanish and leave nothing behind. Because erasure is so nearly total, a runtime does not need a compiler to run a `.ts` file — it only needs to delete the type syntax. Node does exactly that. `--experimental-strip-types` (Node 22.6 and later; on by default in newer releases) replaces the type syntax with whitespace, which keeps every remaining token at its original line and column so stack traces stay accurate with no source map. Notice what it is not doing: it is not type-checking, and it is not transforming anything. ## The constructs that are not erasable A handful of TypeScript features predate the erasure discipline and generate runtime code: ```ts enum Color { Red, Green } // emits a real object with two-way keys namespace Util { export const x = 1 } // emits a function + object class P { constructor(private id: string) {} } // emits this.id = id import fs = require("fs"); // emits a require call ``` Deleting each of these does not preserve meaning — it removes behaviour. `Color` would become an undefined identifier at runtime; `this.id` would never be assigned; the import would disappear. So a strip-only runtime cannot accept them: it errors rather than silently emitting broken code. Handling them requires a *transforming* pipeline — `tsc` itself, or a transpiler such as swc, esbuild or Babel that implements the transforms, or Node's separate `--experimental-transform-types` mode. Decorators sit in the same family for a different reason: they are not type syntax at all, they are a JavaScript-level feature, and a runtime that only erases types has no business rewriting a class. Ambient forms stay fine, because they were never going to emit anything: `declare enum`, `declare namespace`, and everything inside a `.d.ts` file are type-only and erase cleanly. ## The flag that keeps you honest: erasableSyntaxOnly TypeScript 5.8 added `erasableSyntaxOnly`. When it is on, the compiler reports an error on syntax that cannot be erased — the enum, namespace-with-runtime-members, parameter-property, and `import =`/`export =` forms above. It changes nothing about emit; it is a constraint on what you are allowed to write. ```jsonc { "compilerOptions": { "erasableSyntaxOnly": true } } ``` The reason to turn it on is portability of the build topology. If every file in the repo is erasable, then any tool that merely strips types can run or build it — Node directly, `tsx`, esbuild, swc, a test runner's built-in loader — and you are never trapped by one file that happens to need a real transform. It is the same instinct as keeping code free of tool-specific syntax. ## What this costs you The two features people miss most are `enum` and parameter properties. Both have idiomatic erasable replacements: a union of string literals plus a plain `const` object in place of an enum, and explicit field declarations with assignments in place of parameter properties. Some frameworks that lean on constructor parameter properties or on decorator-driven dependency injection are simply not compatible with a strip-only pipeline, and that is a real architectural constraint to check before adopting one. ## How to reason about it in an interview The useful mental test is: *if I delete this syntax, does the program still mean the same thing?* Annotations pass. `enum`, runtime `namespace`, parameter properties and `import =` fail, because their whole purpose is to produce something at runtime. `erasableSyntaxOnly` is the compiler enforcing that test for you, and it exists because strip-only runtimes and transpilers are now the common way TypeScript gets executed.

  • Does turning on erasableSyntaxOnly change the JavaScript that tsc emits?
    No. It is purely a restriction on the source you are allowed to write — the compiler reports an error on non-erasable syntax and emit is otherwise unchanged. Its value is guaranteeing that any strip-only tool can process the same files, not altering output.
  • Why does Node's type stripping replace types with whitespace instead of removing the characters?
    Because it keeps every surviving token at its original line and column. That means runtime stack traces point at the right place in the original `.ts` source without needing a source map, which is a large simplification for a runtime that is deliberately not a compiler.
  • If a dependency in node_modules ships enums, does erasableSyntaxOnly in my repo break?
    No — the flag constrains the source files in your program, and published dependencies normally ship compiled JavaScript plus `.d.ts` files, which contain no runtime-emitting TypeScript syntax to begin with. The constraint applies to the TypeScript you author and run.

saying these in an interview costs you the question

  • Claiming all TypeScript syntax erases with no exceptions
  • Thinking type stripping also type-checks the file
  • Assuming enum is erasable because it looks like a type
  • Confusing erasableSyntaxOnly with a flag that changes emit

context