Ever need to run a TypeScript (TS) entry file from command line directly? Thereāre many tools for that, in fact, probably too many to find the one that just works.
Ā
The real problem is that the requirement varies case by case, itās a no-brainer if you only have a single TS to run against, an IDE plugin is capable of doing the inline evaluation trick, but how about running against a TS file that meets the following conditions?
- Itās within a TS project (with
tsconfig.json
)
- It depends on a mix of ES modules (ESM) & CommonJS modules (CJS) (e.g. importing libraries /
node_modules
distributed in different format)
- It doesnāt use the modern ESM import specifier (i.e. with file extensions)
Problem
Transitioning from CJS to ESM is a pain in the ass, anyone who has done similar migration know what I mean (compare it to Python 2to3). Just to elaborate a bit
- ESM is backward compatible with CJS, but not vice versa. Thatās to use ESM in CJS files, you can only use dynamic import
import()
, which is impossible without significant refactoring
- ESM has the thing called Mandatory file extension which isnāt compatible with how most existing TS codebase is written.
import ... from './foo' // ā how most apps were written import ... from './foo.js' // ā
- Existing tools have their own workarounds long before the standards arrival. Notably popular bundlers like Webpack 2+, Rollup etc are relying on non-official
module
field instead oftype
field in package.json to infer ESM during build phase.
- And more š«± Why CommonJS and ES Modules Canāt Get Along
Common Solution
The most common solution is ts-node per googleās search result, better illustrated by npm trend.
It solves condition #1 above, but left #2, #3 to me still. Because the incompatibility issue I mentioned above, Iāve tried to
- set
"type": "module"
in package.json
- rename file extension to
.mjs
Ā
However, every change only leads to a new problem. To avoid going down the rabbit hole, I turned to other solutions.
Solid Alternatives
tsx
esbuild-kit ā¢ Updated Mar 4, 2023
npx tsx file.ts
After a bit digging around I came across tsx which nailed the problem. Itās a run time based on esbuild known for its build perf compare to other node-native build tools. Itās zero config, thatās you donāt even need a
tsconfig.json
to start using it. One of its biggest selling point to my use case isĀ
It seamlessly adapts between CommonJS and ESM package types by detecting how modules are loaded (require()
Ā orĀimport
) to determine how to compile them. It even adds support forĀrequire()
ing ESM modules from CommonJS so you don't have to worry about your dependencies as the ecosystem migrates to ESM.
Ā
Thatās we donāt have do deal with the fragmented module system issue mentioned above, which is arguably the most painful thing to resolve if you simply want to run some scripts.
Ā
esbuild-register
egoist ā¢ Updated Mar 2, 2023
node -r esbuild-register file.ts
Another tool worth mentioning is esbuild-register.
Similar to tsx, itās a thin wrapper around esbuild. Like babel-register that transpiles ES on the fly using babel, it transpiles TS on the fly using esbuild. Because of that, it shares the same gotcha as esbuild, but nevertheless a great option if your project has esbuild in place already.
Itās not as out-of-box as tsx. But Iāve used it at work, and it easily scales to a Yarn Monorepo with 100+ Workspaces, which is fairly impressive and aligns well with the trend of front-end moving towards Rust / Go written build tools.
Ā
The author of the tsx package has done a more comprehensive comparison between various TS runners. I havenāt used most of them and may not need them for my use case, but they serve as options out there.
ts-runtime-comparison
privatenumber ā¢ Updated Mar 4, 2023
Summary
To recap, if your use case is simple enough that can be generalized, go with ts-node thanks to its full fledged features due to wide usage, but if your use case is more advanced that involves dealing with a legacy codebase, tsx is a better option that addresses the need well and deserves more attention.
Ā
Last but not least, what if the code is written in JS instead of TS, but with the same issue of transitioning from CJS to ESM? Consider giving esm and babel-node a shot.
Ā
Related
- HN thread with some insightful discussions about CJS ā ESM transition
Ā