tnode: simple scripting with Typescript
Arnaud Barré
Front-end engineer
Martin Raison
CTO
Bash and Python are the most popular scripting languages outside the browser. What if you could use Typescript instead?
Solution 1: Deno
For most use cases, Deno is clearly the most powerful option. It is widely used and has no dependency on Node.js. It also comes with many features, such as dependency resolution via arbitrary URLs, and a strong permission model. Using it on macOS is as simple as:
brew install denodeno run myscript.ts
Keep in mind that:
- you need to whitelist permissions explicitly. For many use cases that is an awesome feature. For quick scripting however, this can easily get in the way. This also forces you to jump through a few hoops when creating self-contained scripts with the shebang
- at the time of writing, the Node.js compatibility mode is not stable, and most importantly, does not support TypeScript yet
Solution 2: ts-node
ts-node uses a very different strategy: it transpiles Typescript using tsc, and then directly runs the resulting code with Node.js. This ensures full compatibility with Node.js (unlike Deno), as well as full configurability via tsconfig.json.
You can get started with
npm install -g ts-nodets-node script.ts
Adding #!/usr/bin/env ts-node
at the beginning of your script allows you to run it with ./script.ts
.
Solution 3: tnode
For simpler needs, tnode may be a better option. Here is how it works:
npm install -g @nabla/tnodetnode script.ts
Same as ts-node, add #!/usr/bin/env tnode
to your script to run it with ./script.ts
.
Tnode requires Node.js 16+ and is fully compatible with it. However, it does not perform type checking. This may sound like a big deal, but in practice your IDE already does that. For informal scripting we found it to be a great compromise.
The main conceptual difference with ts-node is that tnode uses esbuild instead of tsc to transpile Typescript. This means it can start running the script much faster. Here is an example with a simple "hello world" script:
$ hyperfine 'tnode script.ts' 'ts-node script.ts'
Benchmark 1: tnode script.ts Time (mean ± σ): 123.1 ms ± 6.5 ms [User: 105.5 ms, System: 22.0 ms] Range (min … max): 116.4 ms … 139.9 ms 22 runs
Benchmark 2: ts-node script.ts Time (mean ± σ): 870.0 ms ± 12.3 ms [User: 1424.7 ms, System: 85.7 ms] Range (min … max): 855.8 ms … 895.7 ms 10 runs
Summary 'tnode script.ts' ran 7.07 ± 0.39 times faster than 'ts-node script.ts'
Under the hood
The cool thing about tnode is its extreme simplicity. You can easily fork it and adapt it to your needs! In fact, we'll put the whole code in this post.
First, we define a transformation that converts Typescript to Javascript using esbuild:
// transform.jsconst { transformSync } = require("esbuild");
module.exports = (src, filename) => { const { code, warnings } = transformSync(src, { loader: "ts", target: "node16", format: "cjs", sourcemap: "inline", sourcefile: filename, }); for (const warning of warnings) console.log(warning.location, warning.text); return code;};
This is a nice example of how you can harness the power of esbuild directly, outside of a toolchain like Vite or Snowpack.
We then use this transform to convert Typescript files (the main script + any imported files) on the fly before running them:
// tnode.js#!/usr/bin/env nodeconst fs = require("fs");const Module = require("module");const { resolve } = require("path");
const transform = require("./transform");
if (process.setSourceMapsEnabled) { process.setSourceMapsEnabled(true);} else { console.warn("Use node >= 16.6 to get source maps support");}
Module._extensions[".ts"] = (mod, filename) => { mod._compile( transform(fs.readFileSync(filename, { encoding: "utf-8" }), filename), filename );};
process.argv.splice(1, 1);process.argv[1] = resolve(process.argv[1]);Module.runMain();
There's a bit of dark magic in there, as we manipulate the internals of the Module
object directly to add support for Typescript files.
Notice how Node.js 16.6+ unlocks experimental source map support. This means if your script runs into an error, you will see a more useful stacktrace with correct line numbers. Neat!
Which one to use?
We clearly recommend Deno or ts-node for:
- production use cases
- complex scripts
- long-term robustness
- configurability
- REPL
- systematic type checking
Deno is more portable (single executable), while ts-node is fully compatible with Node.js.
Tnode is handy when you need:
- low-latency (for a more "shell-like" scripting experience)
- full Node.js compatibility
- simplicity
You can find the tnode repo here: https://github.com/nabla/tnode.
Happy typescripting!
Nabla is building a digital healthcare platform that is made affordable and scalable by machine learning. Come join us!