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.
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
:
NODE_OPTIONS
.NODE_OPTIONS
can be persisted via .env
files..ts
files work like .js
files in that they can be either ESM or CommonJS.
package.json
usually contains "type": "module"
..mts
files are always ESM.
.cts
files are always CommonJS..tsx
files are not supported.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.
These include:
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.
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 to use it in that case.
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
}
}
For larger projects, we probably want to run tsc
and use the compilerOption
called noEmit
.
Let’s take a closer look at the last three options:
allowImportingTsExtensions
[supported since TypeScript 5.0] lets us import TypeScript files (extension .ts
etc.) where we traditionally had to import their transpiled versions (extension .js
etc.).
rewriteRelativeImportExtensions
[supported since TypeScript 5.7] transpiles relative imports from TypeScript files (extension .ts
etc.) to relative imports from JavaScript files (extension .js
etc.).
verbatimModuleSyntax
[supported since TypeScript 5.0] warns us if we don’t use the type
keyword when importing a type.
Option #1 and #2 enable us to use Node.js TypeScript code in other contexts.
--input-type
--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:
module
commonjs
module-typescript
commonjs-typescript
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. 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.
Quoting Jake Bailey (a member of the TypeScript team at Microsoft):
A flag which bans TS features with runtime emit (enums, namespaces, experimental decorators, etc) will come in 5.8 to help people executing TS code via Node.js (or who want to avoid using those features for “reasons”).
--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.