Comparison

JSDoc-typed JavaScript vs Node's type stripping

Node.js now runs .ts files by erasing their types. What that changes about the no-build-step argument, what it does not do, and which Node.js versions do it.

  • Examples checked with TypeScript 5.9

At a glance

Last verified 21 October 2026 with TypeScript 5.9.3 (the examples); Node.js behavior per the Node.js documentation and release notes, not run here.

Compared JavaScript with JSDocTypeScript run by Node.js
Files .js, .mjs, .cjs.ts, .mts, .cts (not .tsx)
Node.js versions that run it Every versionOn by default from 23.6 and 22.18; behind a flag from 22.6; marked stable in 25.2 and 24.12
Type checking when it runs None: the types are commentsNone: Node.js erases the types without checking them
Type checking before it runs tsc with checkJstsc
Syntax allowed All JavaScriptErasable TypeScript only: no enum, no namespace with code, no parameter properties
Imports ./cart.js, as always./cart.ts, with the extension; type-only imports need import type
Reads tsconfig.json No (Node.js never does)No: paths and other options do not apply at runtime
Inside node_modules RunsRefused: packages must ship JavaScript
Browsers and other runtimes Run the same fileNeed a build, or a runtime that strips types itself (Deno, Bun)

What type stripping is

Since version 22.6, Node.js can run a .ts file directly: it replaces the type annotations with whitespace and runs what is left as JavaScript. Per the Node.js documentation and release notes, it arrived behind --experimental-strip-types in 22.6.0 (August 2024), became the default in 23.6.0 (January 2025) and in the 22.x line from 22.18.0 (July 2025), and was marked stable in 25.2.0 (November 2025) and 24.12.0. The documentation lists the feature as stable today. We did not run Node.js for this page; the version history is the documentation’s.

That removes one of the oldest arguments for JSDoc in Node.js code: “TypeScript needs a build step”. For an application or a script on a current Node.js, it no longer does.

What it does not do

Stripping is not checking. Per the Node.js documentation, no type checking is performed: a .ts file with a type error runs, and fails or not at runtime exactly as the equivalent JavaScript would. You still run tsc, in the editor and in CI, exactly as you would for JSDoc-typed JavaScript. The checking half of the workflow is identical; only the syntax of the types and the file extension change.

The documentation lists the other limits:

  • Only erasable syntax. Anything that needs JavaScript written in its place is an error at runtime: enum, a namespace with values in it, parameter properties, import x = require() aliases. Decorators are a parse error. The --experimental-transform-types flag that once handled some of these was removed in Node.js 26.
  • No tsconfig.json. Node.js ignores it, so paths, baseUrl and the rest have no effect at runtime.
  • Explicit extensions and type-only imports. Imports name the .ts file, and an import that brings in only types must say import type, or the program fails when it loads.
  • Not in node_modules. Files under node_modules are not stripped, so packages must still publish JavaScript.

Matching tsc to what Node.js can run

TypeScript 5.8 added erasableSyntaxOnly for exactly this: it reports every construct Node.js cannot erase, as TS1294. With it on, the TypeScript spelling of a module either runs under Node.js or fails tsc. The JavaScript tab of each example below is the JSDoc version, which runs everywhere; the TypeScript tab is checked with erasableSyntaxOnly and verbatimModuleSyntax on:

cart.js cart.ts TypeScript 5.9.3 · checked 21 October 2026
/** @typedef {{ sku: string, quantity: number, unitCents: number }} LineItem */

export const FREE_SHIPPING_CENTS = 5000;

/**
 * @param {LineItem[]} items
 * @returns {number}
 */
export function shippingCents(items) {
  const subtotal = items.reduce((sum, item) => sum + item.quantity * item.unitCents, 0);
  return subtotal >= FREE_SHIPPING_CENTS ? 0 : 500;
}
interface LineItem {
  sku: string;
  quantity: number;
  unitCents: number;
}

export const FREE_SHIPPING_CENTS = 5000;

export function shippingCents(items: LineItem[]): number {
  const subtotal = items.reduce((sum, item) => sum + item.quantity * item.unitCents, 0);
  return subtotal >= FREE_SHIPPING_CENTS ? 0 : 500;
}
JSDoc, and TypeScript that Node.js can run by erasing the types.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["**/*.js"]
}
order.js order.ts TypeScript 5.9.3 · checked 21 October 2026
export const Status = /** @type {const} */ ({ Open: "open", Paid: "paid" });
/** @typedef {(typeof Status)[keyof typeof Status]} StatusValue */

export class Order {
  /**
   * @param {string} id
   * @param {StatusValue} status
   */
  constructor(id, status = Status.Open) {
    this.id = id;
    this.status = status;
  }
}
export enum Status {error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
  Open = "open",
  Paid = "paid",
}

export class Order {
  constructor(public id: string, public status: Status = Status.Open) {}error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
}
An enum and a parameter property cannot be erased; the JSDoc forms can.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["**/*.js"]
}

The JSDoc side needed no special option: JavaScript has no enum and no parameter properties, so the same subset is the only one available. That is the convergence in one sentence: TypeScript code written for type stripping and JavaScript written with JSDoc now avoid the same constructs.

So which?

For code that runs only on Node.js 22.18 or later, the choice is now about syntax and taste: TypeScript annotations are shorter, JSDoc runs everywhere. Two cases still point to JSDoc: packages, because Node.js will not strip types inside node_modules and you publish JavaScript anyway, and code that also runs in browsers or older runtimes without a build. In both, a .js file with JSDoc is the file that ships, and tsc checks it the same way.

Which to choose

Choose JavaScript with JSDoc when…

  • The code is a package: Node.js does not strip types under node_modules, so a published package must be JavaScript anyway.
  • The same files run in browsers or on older Node.js versions without a build.
  • The team reads JavaScript, and wants the types kept in comments next to the code they document.

Choose Node's type stripping when…

  • The code is an application or script that runs only on Node.js 22.18 or later, and you prefer TypeScript syntax.
  • You already write erasable TypeScript, with erasableSyntaxOnly and verbatimModuleSyntax on.
  • You still run tsc in the editor and in CI, because stripping checks nothing.

Questions

Does Node.js type-check .ts files now?

No. Per the Node.js documentation, it replaces the types with whitespace and runs the result; a type error does not stop the program. Checking is still tsc's job, in the editor and in CI, exactly as for JSDoc.

Can I publish a package as .ts files for Node.js to strip?

No. Per the Node.js documentation, type stripping is refused for files under node_modules, so a published package still has to ship JavaScript, with .d.ts files for its types.

What TypeScript settings match what Node.js can run?

TypeScript 5.8 added erasableSyntaxOnly, which reports the syntax Node.js cannot erase (TS1294). With verbatimModuleSyntax and rewriteRelativeImportExtensions, it is the setup the Node.js documentation suggests.

Sources

Last reviewed 21 October 2026.