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 JSDoc | TypeScript run by Node.js |
|---|---|---|
| Files | .js, .mjs, .cjs | .ts, .mts, .cts (not .tsx) |
| Node.js versions that run it | Every version | On 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 comments | None: Node.js erases the types without checking them |
| Type checking before it runs | tsc with checkJs | tsc |
| Syntax allowed | All JavaScript | Erasable 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 | Runs | Refused: packages must ship JavaScript |
| Browsers and other runtimes | Run the same file | Need 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, anamespacewith values in it, parameter properties,import x = require()aliases. Decorators are a parse error. The--experimental-transform-typesflag that once handled some of these was removed in Node.js 26. - No
tsconfig.json. Node.js ignores it, sopaths,baseUrland the rest have no effect at runtime. - Explicit extensions and type-only imports. Imports name the
.tsfile, and an import that brings in only types must sayimport type, or the program fails when it loads. - Not in
node_modules. Files undernode_modulesare 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:
/** @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;
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["**/*.js"]
} 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.
}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
erasableSyntaxOnlyandverbatimModuleSyntaxon. - 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
- Modules: TypeScript (type stripping) · Node.js documentation
- Node.js 22.6.0 (--experimental-strip-types) · Node.js blog
- Node.js 23.6.0 (type stripping on by default) · Node.js blog
- Node.js 22.18.0 (on by default in 22.x) · Node.js blog
- Node.js 25.2.0 (marked stable) · Node.js blog
- TypeScript 5.8: --erasableSyntaxOnly · TypeScript handbook
Last reviewed 21 October 2026.