Node.js Native TypeScript Type Stripping

Run erasable TypeScript directly in Node 24, understand missing type checks and tsconfig support, and retain explicit runtime validation.

Lesson 40 of 43Intermediate10 min read

Node can execute some TypeScript files without a build step, but that convenience has a deliberately limited contract. Removing a type annotation is different from checking that the program obeys it. This guide uses Node 24's built-in erasable syntax support and keeps all runtime logic as ordinary JavaScript. The examples were designed for Node 24.11.1; the documented type-stripping stability designation begins with 24.12.0. Save these fixtures as .mts files and run them with node. The explicit extension makes their ES module format clear without requiring a surrounding package configuration.

Understand what stripping does and does not do

An annotation such as a parameter's string type can be removed without generating replacement runtime logic. Interfaces and type aliases likewise describe static relationships and disappear from execution. Native stripping handles this erasable syntax. It does not check assignment compatibility, prove that a function returns the declared type, or validate data received from a network. A script can execute even though a separate TypeScript checker would report an error.

Keep type checking as an explicit development or continuous-integration step using an appropriate compiler configuration. Running a file successfully tests its runtime behavior on that input, not its entire static correctness. The main fixture accepts unknown data and narrows it through actual JavaScript checks before calculating a total. Its interface describes the validated result for a checker, while the checks protect execution. Replacing those checks with a type assertion would only change static interpretation and would not reject a malformed object at runtime.

Choose compatible syntax and module boundaries

Native execution is designed around erasable syntax. Features that need emitted JavaScript, such as enums, parameter properties, and namespaces containing runtime code, are outside the default stripping path. An experimental transform flag exists in the documented Node 24 line, but it should not be confused with the simple default contract taught here. If a project needs broader TypeScript transformation, use an appropriate compiler or supported runner and test that toolchain deliberately.

File extensions also express module intent. .mts is an ES module and .cts is CommonJS. A .ts file follows the relevant package module rules rather than magically translating one module system into the other. .tsx is not supported by this native path. Import the actual extension for local TypeScript files according to Node's resolution rules. A build that later emits JavaScript needs its own extension and distribution strategy. Node does not infer a project's desired output layout or rewrite all source import paths merely because it recognized a type annotation.

Keep type-only imports explicit

A type exists only for static analysis, while a runtime value must exist when the module loads. import type marks a type-only import so the stripping process can remove it appropriately. Importing an interface as though it were an ordinary runtime export can fail because no corresponding value remains. The same distinction applies to combined imports using the type modifier. Be explicit rather than hoping execution can discover the intended role from a checker that Node is not running.

The native path ignores tsconfig.json. It does not apply TypeScript paths aliases, choose a downlevel JavaScript target, or turn a configured decorator transformation into supported runtime behavior. Package imports beginning with # can provide Node-native private mappings when appropriate, but those are a separate package resolution feature. Node also refuses this native stripping path for TypeScript files under node_modules. Package authors should provide suitable distributed artifacts instead of assuming every consumer runs their source TypeScript through the same local development arrangement.

Retain runtime checks at external boundaries

A declared interface does not make JSON data satisfy it. JSON.parse returns a runtime value that can contain missing properties, strings where numbers were expected, or unexpected nested structures. Start external data as unknown and check the fields your operation requires. Narrowing then lets a type checker understand the validated path. Use a schema-validation library when the data contract becomes complex, but do not remove validation simply because the source file has a TypeScript extension.

Test representative valid and invalid values. Avoid unsafe casts that hide a failing boundary behind an assertion. The practice accepts a quantity only if it is a finite integer in the permitted range; it rejects a numeric-looking string instead of silently coercing it. Whether coercion is desirable is an API design choice, so document it and test it if added. Native execution simplifies one runtime step, while source checks, linting, unit tests, and distribution remain distinct parts of the engineering workflow.

Node.js documentation and version notes

Node.js Native TypeScript Type Stripping Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Running a type-stripped .mts file with runtime checks

Run this complete .mts fixture on the stated Node version. The Line interface and function annotations are erased for execution. parseLine still performs real checks on the unknown input before returning the narrowed shape. The valid object yields a total of twenty-four. The string quantity fails the integer test, demonstrating that a runtime object does not become safe through an annotation. A separate TypeScript compiler can check this source during development, but no compiler package is needed to run the fixture. Its output is evidence for these two runtime inputs, not a claim that Node performed static analysis.

TS
interface Line { name: string; quantity: number; unitPrice: number }
function parseLine(input: unknown): Line {
  if (typeof input !== 'object' || input === null) throw new Error('Invalid line');
  const value = input as Record<string, unknown>;
  if (typeof value.name !== 'string' || !value.name.trim() ||
      typeof value.quantity !== 'number' || !Number.isInteger(value.quantity) || value.quantity < 1 ||
      typeof value.unitPrice !== 'number' || !Number.isFinite(value.unitPrice) || value.unitPrice < 0) {
    throw new Error('Invalid line');
  }
  return { name: value.name, quantity: value.quantity, unitPrice: value.unitPrice };
}
function total(line: Line): number { return line.quantity * line.unitPrice; }
console.log(total(parseLine({ name: 'Mug', quantity: 3, unitPrice: 8 })));
try { parseLine({ name: 'Mug', quantity: '3', unitPrice: 8 }); }
catch (error) { console.log(error instanceof Error ? error.message : 'Unknown error'); }
Output
24
Invalid line

Node.js Native TypeScript Type Stripping in Real Projects

Write a runtime type guard for allowed quantities

Write a type guard for an allowed quantity and use it to process three unknown values. Accept integers from one through ten, reject a string containing digits, and reject a fractional number. The guard must execute real checks; its value is a boolean at runtime and its type predicate communicates narrowing to a checker. The solution performs arithmetic only in the accepted branch. This is a small example of matching the static declaration to observable runtime behavior. If your API deliberately accepts numeric strings, change both the conversion contract and tests rather than using a cast that falsely claims the value was already a number.

TS
function isQuantity(value: unknown): value is number {
  return typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= 10;
}
const inputs: unknown[] = [4, '4', 1.5];
for (const input of inputs) {
  if (isQuantity(input)) console.log(input * 8);
  else console.log('Rejected quantity');
}
Output
32
Rejected quantity
Rejected quantity

Node.js Native TypeScript Type Stripping FAQ

Does Node check my TypeScript types while running?

No. Native stripping removes supported erasable syntax and executes the remaining program. Use a separate checker if you want static diagnostics. A successful runtime fixture does not prove every assignment is type-correct. Conversely, a valid static type does not validate untrusted JSON. Keep runtime boundary checks and source type checking as complementary mechanisms with different jobs.

Will tsconfig paths aliases work automatically?

No. The native execution path ignores tsconfig.json and does not implement its path rewriting or downlevel target behavior. Use Node's actual resolution rules, such as explicit relative extensions or suitable package imports. If your project requires compiler-specific features, run the corresponding build or runner intentionally. A development editor resolving an alias does not establish that Node can load it at runtime.

Why choose .mts for these examples?

It explicitly identifies an ES module regardless of a nearby package's type setting. That makes the standalone fixture predictable. .cts identifies CommonJS, while .ts follows applicable package rules. The extension does not provide type checking or make unsupported syntax valid. When publishing compiled JavaScript, plan the emitted extensions and import specifiers separately instead of assuming the native source-file arrangement is the distribution format.

Can I publish source TypeScript under node_modules?

Do not assume consumers can run it through this native path. Node refuses native stripping for TypeScript files under node_modules. Provide appropriate artifacts or a documented toolchain for the package's supported consumers. A local script running directly from your project is a different situation. The package boundary also needs module-format and exports decisions, which are covered in the package reference.

Node.js Native TypeScript Type Stripping Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choiceeasy

Which worked inputs pass isQuantity?

Node.js Native TypeScript Type Stripping Quiz

2 questions to check what you learned.

  1. What does a TypeScript assertion do to untrusted JSON at runtime?

    • It does not validate or convert the value
    • It checks every property against the interface
    • It automatically rejects unknown fields
  2. Which feature is outside default erasable stripping?

    • An enum requiring generated JavaScript
    • A type alias
    • A parameter type annotation