Skip to main content

CommonJS vs native ECMAScript modules

TypeScript is almost always written using modern import syntax, but it is also transformed before being executed by the underlying runtime. You can choose to either transform to CommonJS or to preserve the native import syntax, using node's native ESM support. Configuration is different for each.

Here is a brief comparison of the two.

CommonJSNative ECMAScript modules
Write native import syntaxWrite native import syntax
Transforms import into require()Does not transform import
Node executes scripts using the classic CommonJS loaderNode executes scripts using the new ESM loader
Use any of:
ts-node
node -r ts-node/register
NODE_OPTIONS="ts-node/register" node
require('ts-node').register({/* options */})
Use any of:
ts-node --esm
ts-node-esm
Set "esm": true in tsconfig.json
node --loader ts-node/esm
NODE_OPTIONS="--loader ts-node/esm" node

CommonJS

Transforming to CommonJS is typically simpler and more widely supported because it is older. You must remove "type": "module" from package.json and set "module": "CommonJS" in tsconfig.json.

package.json
json
{
// This can be omitted; commonjs is the default
"type": "commonjs"
}
package.json
json
{
// This can be omitted; commonjs is the default
"type": "commonjs"
}
tsconfig.json
json
{
"compilerOptions": {
"module": "CommonJS"
}
}
tsconfig.json
json
{
"compilerOptions": {
"module": "CommonJS"
}
}

If you must keep "module": "ESNext" for tsc, webpack, or another build tool, you can set an override for ts-node.

tsconfig.json
json
{
"compilerOptions": {
"module": "ESNext"
},
"ts-node": {
"compilerOptions": {
"module": "CommonJS"
}
}
}
tsconfig.json
json
{
"compilerOptions": {
"module": "ESNext"
},
"ts-node": {
"compilerOptions": {
"module": "CommonJS"
}
}
}

Native ECMAScript modules

Node's ESM loader hooks are experimental and subject to change. ts-node's ESM support is as stable as possible, but it relies on APIs which node can and will break in new versions of node. Thus it is not recommended for production.

For complete usage, limitations, and to provide feedback, see #1007.

You must set "type": "module" in package.json and "module": "ESNext" in tsconfig.json.

package.json
json
{
"type": "module"
}
package.json
json
{
"type": "module"
}
tsconfig.json
json
{
"compilerOptions": {
"module": "ESNext" // or ES2015, ES2020
},
"ts-node": {
// Tell ts-node CLI to install the --loader automatically, explained below
"esm": true
}
}
tsconfig.json
json
{
"compilerOptions": {
"module": "ESNext" // or ES2015, ES2020
},
"ts-node": {
// Tell ts-node CLI to install the --loader automatically, explained below
"esm": true
}
}

You must also ensure node is passed --loader. The ts-node CLI will do this automatically with our esm option.

Note: --esm must spawn a child process to pass it --loader. This may change if node adds the ability to install loader hooks into the current process.

shell
# pass the flag
ts-node --esm
# Use the convenience binary
ts-node-esm
# or add `"esm": true` to your tsconfig.json to make it automatic
ts-node
shell
# pass the flag
ts-node --esm
# Use the convenience binary
ts-node-esm
# or add `"esm": true` to your tsconfig.json to make it automatic
ts-node

If you are not using our CLI, pass the loader flag to node.

shell
node --loader ts-node/esm ./index.ts
# Or via environment variable
NODE_OPTIONS="--loader ts-node/esm" node ./index.ts
shell
node --loader ts-node/esm ./index.ts
# Or via environment variable
NODE_OPTIONS="--loader ts-node/esm" node ./index.ts