Β· hands on

Fixing TypeError ERR_UNKNOWN_FILE_EXTENSION with ts-node

When using TypeScript with Node.js and ts-node, you might face "ERR_UNKNOWN_FILE_EXTENSION." To fix this, you have to use ts-node with specific commands for Node.js module loaders and ESM projects. Consider also alternatives like tsx and tsimp.

When building projects with TypeScript, you might encounter "ERR_UNKNOWN_FILE_EXTENSION" because runtimes like Node.js expect JavaScript and not plain TypeScript code. To fix this, you use ts-node, which runs TypeScript in Node.js. It’s downloaded over 30 million times weekly and is trusted by the TypeScript community. Previously, running ts-node src/main.ts was enough to get up and running, but with ECMAScript Modules and Node.js v20+, this will cause the "ERR_UNKNOWN_FILE_EXTENSION" error. Let's see how to fix this.

Contents

TL;DR

  • The ts-node package struggles with ESM projects and Node.js v20+ runtimes
  • The ts-node team is seeking sponsorship to resolve the ESM incompatibilities
  • You can switch to tsx or tsimp as alternatives to ts-node
  • However, tsx doesn't support type checking and tsimp has caching issues
  • A recommended approach is to use tsc for type checking and ts-node for transpiling
  • Transpiling with tsccan be done like this: tsc --noEmit
  • Compiling with ts-node on Node.js 20+ with ESM support can be done like this: node --no-warnings=ExperimentalWarning --loader ts-node/esm/transpile-only src/main.ts
  • Combined command: tsc --noEmit && node --no-warnings=ExperimentalWarning --loader ts-node/esm/transpile-only src/main.ts
  • Note that the --loader flag is experimental and deprecated as of Node.js v20.6.0
  • If you want to be future-proof, better use tsc --noEmit && tsx src/main.ts

How ts-node was working in the past

For a CommonJS project using Node.js 18 with ts-node, you can run your code using the following command:

ts-node main.ts

For an ECMAScript Modules (ESM) project with Node.js 18, you need to use:

ts-node-esm main.ts

However, when using Node.js 20 and ts-node-esm, you may encounter the following error:

TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /home/bennycode/dev/main.ts

To work with ESM and ts-node-esm in Node.js 20 and above, you must use the Node.js loader flag:

node --loader ts-node/esm src/main.ts

Unfortunately, this method doesn't provide stack traces. In the event of a compiler error, you'll only see an output like this:

node:internal/process/esm_loader:40
internalBinding('errors').triggerUncaughtException
[Object: null prototype] {
  [Symbol(nodejs.util.inspect.custom)]: [Function: [nodejs.util.inspect.custom]]
}

How to Make ts-node Work

The "ERR_UNKNOWN_FILE_EXTENSION" error can show up when using ts-node with ECMAScript Modules (ESM) and recent Node.js versions. This is because ts-node v10.9.2 isn't fully compatible with Node.js v20 and ESM (see GitHub issue). That's why ts-node-esm cannot be used directly. Instead, you'll need to use Node.js's --loader flag:

node --loader ts-node/esm src/main.ts

The Problem with the Loader Flag

The --loader flag is experimental and was deprecated in Node.js v20.6.0 (see GitHub issue). To suppress the warning you can filter out experimental issues by running:

node --no-warnings=ExperimentalWarning --loader ts-node/esm src/main.ts

While this allows your TypeScript code to run on Node.js, it introduces another limitation: you won't get proper stack traces if your code fails. This is a known issue with the current version of ts-node (v10.9.2).

Blake Embrey, the creator of ts-node, has raised a sponsorship issue to rewrite the ESM loading APIs. Until a proper fix is available, you can use TypeScript's original compiler (tsc) to ensure stack traces. Configure tsc to perform type checking without emitting JavaScript by enabling the noEmit flag. Then, execute tsc and ts-node together:

tsc --noEmit && node --no-warnings=ExperimentalWarning --loader ts-node/esm src/main.ts

Disabling Type Checking in ts-node

Since type checking in ts-node is currently broken, you can disable it by enabling transpilation-only mode. Normally, you would run:

ts-node-esm --transpileOnly src/main.ts

However, you can't pass the --transpileOnly flag to Node.js when using --loader ts-node/esm, as it results in this error:

node: bad option: --transpileOnly

In that case, you can use the TS_NODE_TRANSPILE_ONLY environment variable:

TS_NODE_TRANSPILE_ONLY=true tsc --noEmit && node --no-warnings=ExperimentalWarning --loader ts-node/esm src/main.ts

Handling Environment Variables on Windows

On native Windows systems, you'll need a tool like cross-env to set the environment variable:

cross-env TS_NODE_TRANSPILE_ONLY=true tsc --noEmit && node --no-warnings=ExperimentalWarning --loader ts-node/esm src/main.ts

Configuring ts-node through tsconfig

As installing another dependency just for the sake of exporting a configuration value isn't ideal, you can configure the transpiling mode for ts-node inside of your tsconfig.json file:

tsconfig.json
{
  "ts-node": {
    "transpileOnly": true
  },
  "compilerOptions": {
    //
  }
}

This, however, is also an anti-pattern because configurations of external tools get mixed with TypeScript's compiler configuration.

The Simplest Solution

Fortunately, the ts-node package includes a loading module called ts-node/esm/transpile-only, which resolves all the issues mentioned above. Thus, the final command will be:

tsc --noEmit && node --no-warnings=ExperimentalWarning --loader ts-node/esm/transpile-only src/main.ts

This approach avoids extra dependencies, suppresses loader warnings, enables type checking and ensures your project runs smoothly with TypeScript on Node.js 20+ with support for ECMAScript Modules.

Solution for nodemon

Users of nodemon can set up this configuration within the nodemon.json config file:

nodemon.json
{
  "watch": ["src"],
  "ext": "ts",
  "execMap": {
    "ts": "tsc --noEmit && node --no-warnings=ExperimentalWarning --loader ts-node/esm/transpile-only"
  }
}

Your program's executable will then be: nodemon src/main.ts

The Future of ts-node

The latest version of ts-node was released on December 8, 2023, nearly a year from the time of writing this article. While ts-node is a fantastic tool, it requires improved ESM support. This feature has been the most requested since 2020 and is currently facing over 420 comments on the issue thread. As long as this is an outstanding topic, there are a few options to consider.

Alternative: tsx

The tsx tool is in active development and can run TypeScript code but it doesn't provide type checking capabilities (tried with tsx v4.19.2). In order to make the best use of TypeScript, you'll need to combine it with tsc:

tsc --noEmit && tsx src/main.ts

The great thing about using tsx is that you don't have to rely on Node.js's deprecated --loader flag.

Alternative: tsimp

The tsimp loader was initiated by Isaac Z. Schlueter, who also wrote npm. It is designed to provide complete type-checking support and can be used as follows:

TSIMP_DIAG=error node --import=tsimp/import src/main.ts

When using it (tsimp 2.0.12) I discovered caching issues that are difficult to debug. Therefore, I do not recommend it for production use at this time.

Alternative: Deno

Deno offers first-class support for TypeScript. Running a TypeScript program on Deno is as simple as this:

deno src/main.ts

Be aware that Deno is a completely different environment to Node.js, so you can have a lot of compatibility issues when trying to run your aged Node.js projects on Deno. Yet, Deno has the most incredible video showing how "effortless" it is to set up a TypeScript project in 2024.

Video Tutorial

Back to Blog