Skip to content

Commit f0e89b2

Browse files
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.
1 parent f8992f1 commit f0e89b2

1 file changed

Lines changed: 19 additions & 0 deletions

File tree

notes/node/type-stripping.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,24 @@ Node's built-in TypeScript execution pipeline, how it evolved, and which remaini
2424
- 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)).
2525
- 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).
2626
- `--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.
2745

2846
## What Node intentionally does not do
2947

@@ -70,6 +88,7 @@ Native stripping covers the common, erasable-syntax case. That is the ideal path
7088
- 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);
7189
- tsx handles CJS/ESM interop and named exports around transformed source (see [`cjs-esm-interop.md`](./cjs-esm-interop.md));
7290
- 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.
7392

7493
## Remaining gaps Node is closing
7594

0 commit comments

Comments
 (0)