- Update 2025-01-29:
--erasableSyntaxOnlyis available in TypeScript 5.8 Beta. - Update 2025-01-09: Added the section “A few random thoughts”.
Starting with v23.6.0, Node.js supports TypeScript without any flags. This blog post explains how it works and what to look out for.
Would you rather jump right in and explore?
Then take a look at my GitHub repository nodejs-type-stripping.
A first look at the new feature
Consider the following file:
// demo.mts
function main(message: string): void {
console.log('Message: ' + message);
}
main('Hello!');
We can now run it like this:
node demo.mts
For now, we get the following warning:
ExperimentalWarning: Type Stripping is an experimental feature and
might change at any time
We can switch off that warning:
node --disable-warning=ExperimentalWarning demo.mts
Tips for using --disable-warning:
- We can make this flag a default by setting up the environment variable
NODE_OPTIONS. - Environment variables such as
NODE_OPTIONScan be persisted via.envfiles.
Filename extensions
.tsfiles work like.jsfiles in that they can be either ESM or CommonJS.- This is a good choice for files in projects – whose
package.jsonusually contains"type": "module".
- This is a good choice for files in projects – whose
.mtsfiles are always ESM.- Use this filename extension for standalone files.
.ctsfiles are always CommonJS..tsxfiles are not supported.
How is Node.js TypeScript different from normal TypeScript?
Current TypeScript support in Node.js is done via type stripping: All Node.js does is remove all syntax related to types. It never transpiles anything. Let’s explore how that changes how we write TypeScript.
No support for non-JavaScript language features
These include:
- Enums
- Parameter properties in class constructors.
- Namespaces
No support for JSX
Neither .tsx files nor JSX are supported.
No support for future JavaScript that is compiled to current JavaScript
TypeScript supports some upcoming JavaScript features and transpiles them so that they run on current JavaScript engines. One such feature is decorators. Those will be supported by Node.js in TypeScript when they are supported in JavaScript.
Local imports must refer to TypeScript files
In traditional TypeScript, we refer to the transpiled versions of modules:
import { myFunction } from './my-module.js';
Why is that? Traditionally, the TypeScript compiler never touched module specifiers such as './my-module.js'. Therefore, we had to use module specifiers that made sense in the transpiled output.
Given that Node.js uses the filename extension to determine the type of a module, this approach had to change. We now have to write:
import { myFunction } from './my-module.ts';
I personally prefer this approach even for code that is not meant to run on Node.js. The section on tsconfig.json explains how can transpile such code to JavaScript.
Types must be imported via type imports
If we want to import types, we have to use type imports – otherwise type stripping won’t remove them.
// Type import
import type { Cat, Dog } from './animal.ts';
// Inline type import
import { createCatName, type Cat, type Dog } from './animal.ts';
tsconfig.json
Node’s type stripping ignores tsconfig.json but if we want to type-check during development, we need one. This is a minimal setup recommended by the Node.js documentation:
{
"compilerOptions": {
"target": "esnext",
"module": "nodenext",
"allowImportingTsExtensions": true,
"rewriteRelativeImportExtensions": true,
"verbatimModuleSyntax": true,
"erasableSyntaxOnly": true // TS 5.8+
}
}
For larger projects, we probably want to run tsc and use the compilerOption called noEmit.
Let’s take a closer look at the last four options:
-
allowImportingTsExtensions[supported since TypeScript 5.0] lets us import TypeScript files (extension.tsetc.) where we traditionally had to import their transpiled versions (extension.jsetc.). -
rewriteRelativeImportExtensions[supported since TypeScript 5.7] transpiles relative imports from TypeScript files (extension.tsetc.) to relative imports from JavaScript files (extension.jsetc.). -
verbatimModuleSyntax[supported since TypeScript 5.0] warns us if we don’t use thetypekeyword when importing a type. -
erasableSyntaxOnly[supported since TypeScript 5.8] warns us if we use TypeScript features that are not supported by type stripping: mainly enums, parameter properties in class constructors, namespaces and JSX.
Option #1 enables type checking: Without it, TypeScript wouldn’t find imported files.
Option #2 enables transpilation of Node.js TypeScript to JavaScript.
CLI option --input-type
The CLI option --input-type tells Node.js how to interpret code when it doesn’t come from a file (where the filename extension contains that information) – i.e., when it comes from stdin or --eval. This flag now supports the following values:
modulecommonjsmodule-typescriptcommonjs-typescript
Example:
echo 'console.log("Hello" as const)' | node --input-type module-typescript
Type stripping and source maps
ts-blank-space by Ashley Claymore for Bloomberg pioneered a clever approach for type stripping: If, instead of simply removing all text related to types, we “overwrite” it with spaces then source code positions in the output don’t change and stack traces etc. remain correct. Therefore, there is no need for source maps.
For example - input (TypeScript):
function describeColor(color: Color): string {
return `Color named “${color.colorName}”`;
}
type Color = { colorName: string };
describeColor({ colorName: 'green' });
Output (JavaScript):
function describeColor(color ) {
return `Color named “${color.colorName}”`;
}
describeColor({ colorName: 'green' });
Note the empty line between the declaration of describeColor() and its invocation.
Node.js type stripping uses the same approach and therefore does not generate source maps.
What’s next?
--experimental-transform-types
The work-in-progress feature --experimental-transform-types will actually transpile TypeScript and therefore support more features. It will generate source maps and enable source maps by default.
How will this feature end up being used? It’ll be opt-in, with the default being type stripping – as it is now (source: Marco Ippolito).
A few random thoughts
-
I’m ambivalent about TypeScripts many filename extensions (without having anything better to offer):
.ts,.tsx,.mts,.cts, but they do come in handy with Node.js. -
It feels like type stripping should be able to directly feed the pruned parsed code into the V8 JavaScript engine. Maybe that’s the future of TypeScript support in browsers? Support type-stripping and not syntactic extensions for JavaScript.
-
With type stripping becoming popular: What is going to happen to JSX? Will replace it with alternatives such as the template tag
htm?
More information
- Source of this blog post: “Modules: TypeScript” in the Node.js documentation
- Blog post by Marco Ippolito (who implemented the feature): “Everything You Need to Know About Node.js Type Stripping”