You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs: document Node's public type-stripping API vs internal loader path
## Problem
`notes/node/type-stripping.md` covers the loader pipeline but not the
boundary that matters for integrating with it: the public
`module.stripTypeScriptTypes()` API behaves differently from the
internal loader path, and several load-bearing details (hook-chain
ordering, typeless-package classification, compile-cache keying, output
stability) were verified against source but undocumented.
## Changes
- New section comparing the public API vs `stripTypeScriptModuleTypes`:
node_modules restriction and compile-cache integration are
internal-path-only; `mode: 'transform'` is accepted flaglessly through
v25.x and removed from the public API at v26; the ExperimentalWarning
survives into v26
- Runtime pipeline additions: stripping runs *after* the module-hook
chain (translators keyed on final format), typeless-package `.ts`
classification follows the `.js` contract with detection at load time
(resolve reports `null` format), and compile-cache/V8 code cache keying
applies to hook-provided sources
- Error-contract detail: `ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX` vs
`ERR_INVALID_TYPESCRIPT_SYNTAX`; decorators pass through as JavaScript;
`erasableSyntaxOnly` flags exactly the unsupported-syntax set
- Output-stability warning from `module.md` (don't key caches/assertions
on exact bytes)
- Relationship-to-tsx: notes the module-classification difference for
typeless packages (Node syntax-detects; tsx's detection predates
detect-module and classifies as CommonJS)
All line anchors verified against the nodejs/node clone at v24.15.0 /
v26.4.0.
Copy file name to clipboardExpand all lines: notes/node/type-stripping.md
+19Lines changed: 19 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -24,6 +24,24 @@ Node's built-in TypeScript execution pipeline, how it evolved, and which remaini
24
24
- ESM format detection maps `.ts` to `module-typescript` / `commonjs-typescript` based on package type and syntax detection ([v24.0.0 `get_format.js#L133-L149`](https://github.com/nodejs/node/blob/v24.0.0/lib/internal/modules/esm/get_format.js#L133-L149)). In v26 the extension map is explicit for `.ts` / `.mts` / `.cts` ([`get_format.js#L31-L33`](https://github.com/nodejs/node/blob/v26.0.0/lib/internal/modules/esm/get_format.js#L31-L33)) before the same package/syntax logic later in the file ([`get_format.js#L189-L202`](https://github.com/nodejs/node/blob/v26.0.0/lib/internal/modules/esm/get_format.js#L189-L202)).
25
25
- The CommonJS loader strips TS formats before compile. v24.0.0 handles `module-typescript` / `commonjs-typescript` / `typescript` at [`loader.js#L1125-L1127`](https://github.com/nodejs/node/blob/v24.0.0/lib/internal/modules/cjs/loader.js#L1125-L1127), and v26 has the same strip-before-compile path via `stripTypeScriptModuleTypes` at [`loader.js#L182`](https://github.com/nodejs/node/blob/v26.0.0/lib/internal/modules/cjs/loader.js#L182) and the TS-format cases at [`loader.js#L1125-L1127`](https://github.com/nodejs/node/blob/v26.0.0/lib/internal/modules/cjs/loader.js#L1125-L1127).
26
26
-`--eval` and STDIN use the same internal TypeScript evaluator: v22.14.0 wires `evalTypeScript` into `eval_string` ([`eval_string.js#L39-L68`](https://github.com/nodejs/node/blob/v22.14.0/lib/internal/main/eval_string.js#L39-L68)) and `eval_stdin` ([`eval_stdin.js#L37-L47`](https://github.com/nodejs/node/blob/v22.14.0/lib/internal/main/eval_stdin.js#L37-L47)).
27
+
- Stripping runs after the module-hook chain, not before it. The ESM loader loads source through the hook chain first, then translates by the final format ([v24.15.0 `loader.js#L408-L414`](https://github.com/nodejs/node/blob/v24.15.0/lib/internal/modules/esm/loader.js#L408-L414)); the `commonjs-typescript` and `module-typescript` translators call `stripTypeScriptModuleTypes` on whatever source the hooks returned ([v24.15.0 `translators.js#L628-L643`](https://github.com/nodejs/node/blob/v24.15.0/lib/internal/modules/esm/translators.js#L628-L643)). A load hook that returns TypeScript source with a `-typescript` format gets native stripping; a hook that rewrites the format opts out of it.
28
+
- For `.ts` files in a package without a `type` field, the format follows the same contract as `.js`, including syntax detection. Detection happens at load time, not resolve time: `defaultResolve` reports a `null` format for these files, and the module system is determined when the source is read.
29
+
- The transpiled output participates in the compile cache and V8 code cache keyed by source text and URL at the C++ layer ([v24.15.0 `module_wrap.cc#L518-L527`](https://github.com/nodejs/node/blob/v24.15.0/src/module_wrap.cc#L518-L527)), so any module compilation — including source produced by customization hooks — is cacheable when the compile cache is enabled.
30
+
31
+
## Public `stripTypeScriptTypes()` vs the internal loader path
32
+
33
+
`node:module` exposes stripping as a public API, but it is a different code path from what the loaders use, with different behavior:
34
+
35
+
| Aspect | Public `stripTypeScriptTypes()`| Internal `stripTypeScriptModuleTypes()`|
36
+
| --- | --- | --- |
37
+
|`node_modules`| No restriction | Refuses with `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING` ([v24.15.0 `typescript.js#L180-L183`](https://github.com/nodejs/node/blob/v24.15.0/lib/internal/modules/typescript.js#L180-L183)) |
38
+
| Compile cache | Not used | Keyed by filename via `getCompileCacheEntry` ([v24.15.0 `typescript.js#L198`](https://github.com/nodejs/node/blob/v24.15.0/lib/internal/modules/typescript.js#L198)) |
39
+
| Mode |`'strip'` or `'transform'` accepted without any CLI flag through v25.x ([v24.15.0 `typescript.js#L112`](https://github.com/nodejs/node/blob/v24.15.0/lib/internal/modules/typescript.js#L112)); v26 accepts only `'strip'` ([v26.4.0 `typescript.js#L101`](https://github.com/nodejs/node/blob/v26.4.0/lib/internal/modules/typescript.js#L101)) | Follows `--experimental-transform-types` until its removal in v26 |
40
+
| Warning | Emits an `ExperimentalWarning` on first call, still present in v26 ([v26.4.0 `typescript.js#L92`](https://github.com/nodejs/node/blob/v26.4.0/lib/internal/modules/typescript.js#L92)) | None |
41
+
42
+
In strip mode, types are replaced with whitespace so line and column positions match the source and no source map is produced; `sourceMap: true` is only valid in transform mode. The docs warn that output should not be considered stable across Node versions ([v24.15.0 `module.md#L281`](https://github.com/nodejs/node/blob/v24.15.0/doc/api/module.md#L281)), so consumers should not key caches or assertions on exact output bytes.
43
+
44
+
Amaro reports two error classes, surfaced as distinct codes: `ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX` for valid TypeScript that requires transformation (`enum`, runtime `namespace`, parameter properties, `import =`/`export =` aliases), and `ERR_INVALID_TYPESCRIPT_SYNTAX` for source that does not parse. Decorators are neither: they are treated as JavaScript syntax and pass through stripping untouched, so they reach V8 as-is. TypeScript's `erasableSyntaxOnly` checker option flags exactly the unsupported-syntax set — not decorators or `accessor`, which are checker-legal JavaScript — so a project that type-checks under `erasableSyntaxOnly` never triggers the unsupported-syntax error at runtime.
27
45
28
46
## What Node intentionally does not do
29
47
@@ -70,6 +88,7 @@ Native stripping covers the common, erasable-syntax case. That is the ideal path
70
88
- tsx implements extension and path behavior Node explicitly leaves to tools (`paths`, extensionless imports, `.js` specifiers that map to `.ts`, TypeScript under dependency graphs when needed);
71
89
- tsx handles CJS/ESM interop and named exports around transformed source (see [`cjs-esm-interop.md`](./cjs-esm-interop.md));
72
90
- tsx's benchmark `native-ts` scenario measures the runtime floor Node provides, while `esm-ts` measures the extra transform and resolution surface tsx adds for those gaps.
91
+
- Module-system classification differs for one population: for `.ts` files in a package without a `type` field, Node applies the same syntax detection it applies to `.js`, while tsx's format detection predates detect-module and classifies those files as CommonJS. Code relying on ESM-only semantics (such as top-level await) in a typeless package runs under plain `node` but is converted to CommonJS by tsx.
0 commit comments