A TypeScript Import loader for Node.js
This is an importer that runs Node.js programs written in TypeScript, using the official TypeScript implementation from Microsoft.
It is designed to support full typechecking support, with acceptable performance when used repeatedly (for example, in a test suite which spawns many TS processes).
There are quite a few TypeScript loaders and compilers available! Which one should you choose, and why did I need to create this one?
- swc is a TypeScript compiler implementation in Rust
- tsx is a zero-config TypeScript executer that aims to be a drop-in replacement for node, powered by esbuild.
- ts-node is probably the most established of these, with a huge feature set and support for every version of node and TypeScript you could possibly want.
How this differs:
- It uses the TypeScript implementation from Microsoft as its
compiler. No shade towards swc and esbuild, they're fast and
can do a lot, but the goal of
tsimp
is strict consistency with the "official"tsc
program, and just using it is the simplest way to do that. - It supports the
--import
andModule.register()
behavior added in node v20.6, only falling back to warning-laden experimental APIs when that's not available. - Type checking is enabled by default, so no need to run an extra
tsc --noEmit
step after running tests, using a persistentLanguageService
daemon and a ridiculous amount of caching to make it performant. - It's just a module loader, not a bunch of other things. So there's no repl, no bundler, etc. It's not a lot of code (unless you count TypeScript itself, which to be fair, is kind of a lot of code, but you're probably already biting that bullet).
Install tsimp
with npm:
npm install tsimp
Run TypeScript programs like this in node v20.6 and higher:
node --import=tsimp/import my-typescript-program.ts
Or like this in Node versions prior to v20.6:
node --loader=tsimp/loader my-typescript-program.ts
Or you can use tsimp
as the executable to run your program (but
the import/loader is ~100ms faster because it doesn't incur an
extra spawn
call):
tsimp my-typescript-program.ts
Note that while tsimp
run without any arguments will start the
Node repl, and in that context it will be able to import/require
typescript modules, it does not include a repl that can run
TypeScript directly. This is just an import loader.
In Node v20.6 and higher, you can also load tsimp
in your
program, and from that point forward, TypeScript modules will
Just Work.
import 'tsimp'
// has to be done as an async import() so that it occurs
// after the tsimp import is finished. But any imports that the
// typescript program does can be "normal" top level imports.
const { SomeThing } = await import('./some-thing.ts')
CommonJS require()
is patched as well. To use tsimp
in
CommonJS programs, you can run it as described above, or
require()
it in your program.
//commonjs
require('tsimp')
// now typescript can be loaded
require('./blah.ts')
In Node version 20.6 and higher, this will also attach the
required loaders for ESM import support. In earlier Node
versions, you must use --loader=tsimp/loader
for ESM support.
The same rules for file extensions, module resolution, and
everything else apply when using tsimp
as when using tsc
.
That is, if you're running in ESM mode, you may need to write
your imports ending in .js
even though the actual file on disk
is .ts
, because that's how TS does it.
Most configuration is done by looking to the nearest
tsconfig.json
file at or above the module entry point in the
folder tree.
You can use a different filename by setting
TSIMP_PROJECT=<filename>
in the environment.
If there is a tsimp
field in the tsconfig json file, then that
will override anything else in the file. For example:
{
"compilerOptions": {
"rootDir": "./src",
"declaration": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"inlineSources": true,
"jsx": "react",
"module": "nodenext",
"moduleResolution": "nodenext",
"noUncheckedIndexedAccess": true,
"resolveJsonModule": true,
"skipLibCheck": false,
"sourceMap": false,
"strict": true,
"target": "es2022"
}
"tsimp": {
"compilerOptions": {
"sourceMap": true,
"skipLibCheck": true
}
}
}
Sourcemaps are always enabled when using tsimp
, so that errors
reference the approriate call sites within TypeScript code.
The ultimate resulting module style for tsimp must be something
intelligible by Node, without any additional bundling or
transpiling. So, unless it's going to be made Node-compatible by
some other loader/import script in the stack, you'll need to set
your module
tsconfig for tsimp
to something like Node16
or
NodeNext
, even if you are actually compiling with tsc in
"module": "bundler"
mode.
{
"compilerOptions": {
"...": "..etc..",
"module": "bundler"
},
"tsimp": {
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}
}
Set the TSIMP_DIAG
environment variable to control what happens
when there are compilation diagnostics.
TSIMP_DIAG=warn
(default) Print diagnostics tostderr
, but still transpile the code if possible.TSIMP_DIAG=error
Print diagnostics tostderr
, and fail if there are any diagnostics.TSIMP_DIAG=ignore
Just transpile the code, ignoring all diagnostics. (Similar to ts-node'sTS_NODE_TRANSPILE_ONLY=1
option.)