Minimalistic example of configuring TypeScript and Node to:
- emit modern ES modules code
- import modules that use Node built-ins
- import modules that don't have named exports (e.g.
apollo-server
,node-influx
) - import your own modules without specifying an extension
- lint with ESLint, with TypeScript support
- test the TypeScript code instantly without having to build first
- run the resulting JavaScript code, with support for the optional chaining operator
?.
Bonus: continuous integration script for GitHub Actions. It automatically runs tests on every pushed commit.
There is no need for Babel.
In tsconfig.json
, set this in compilerOptions
:
"target": "esnext",
"module": "esnext", // Output `import`/`export` ES modules
- run
npm install --save-dev @types/node
- in
tsconfig.json
undercompilerOptions
, set"moduleResolution": "node"
, sotsc
can find modules when targeting ES6+"types": ["node"]
to avoid errors related to Node built-in modules
Normally we could write in TypeScript
import { InfluxDB } from 'influx';
but when generating ES modules code, that statement will be passed through as is, and will cause Node to fail with
SyntaxError: The requested module 'influx' does not provide an export named 'InfluxDB'
because node-influx
doesn't provide named exports (and neither does an even more popular module, apollo-server
).
One alternative would be to generate old ugly commonjs modules code by,
- removing the
"type": "module"
line frompackage.json
, and - changing the module line to
"module": "CommonJS"
intsconfig.json
(allowSyntheticDefaultImports
also becomes unnecessary)
What we'll do is import the entire module:
import Influx from 'influx';
const influx = new Influx.InfluxDB();
However, this will generate Error TS1192: Module '...' has no default export.
To prevent that, set "allowSyntheticDefaultImports": true
in tsconfig.json
.
When transpiling, TypeScript won't generate an extension for you. Run Node with the node --experimental-specifier-resolution=node
parameter:
node --experimental-specifier-resolution=node run.js
Otherwise, node mandates that you specify the extension in the import
statement.
To support optional chaining, add the --harmony
flag to the node command line.
Add "type": "module"
to package.json
, because TypeScript can't generate files with the .mjs extension.
To be able to run eslint
, we must create an .eslintrc.cjs
file, rather than a .js
one (due to "type": "module"
in package.json
). Then, install the required dependencies:
npm i -D eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser
Here's the diff to add ESLint support.
The cleanest way to fully support Jest with TypeScript, ES Modules and ESLint, is to use ts-jest
, which has a number of advantages over using Babel. What we need to do:
npm install --save-dev jest @types/jest eslint-plugin-jest
- for ES Lint support- add
"jest"
to thetypes
array intsconfig.json
- add the
'jest'
plugin to.eslintrc.cjs
and also add'jest/globals': true
to itsenv
key - use the
jest.config.cjs
generated by ts-jestconfig:init
(just renamed .js -> .cjs).
Normally, to run Jest from package.json
, we'd add a "test": "jest"
line. That won't be sufficient, because we need to pass the --harmony
flag to node (for optional chaining support).
To pass parameters to Node when running Jest, we'll add the following test
line:
"test": "node --harmony node_modules/.bin/jest"
The only caveat here is that Jest seems to prefer generated .js
files over their .ts
originals, so we'll exclude them via jest.config.cjs
:
testRegex: '.*.test.ts', // test filenames matching this regex
moduleFileExtensions: ['ts', 'js'], // modules are only in .ts files, but 'js' *must* be specified too
If your script generates an error, you'll see the line numbers from the generated .js
files, which is not helpful. We want to see the original paths and line numbers from the .ts
files. To do that, we'll add sourceMap: true
to tsconfig.json
, install source-map-support
and run node with the -r source-map-support/register
parameter. Note that Jest already takes care of source mapping so you'll see the .ts
line numbers without having to do anything extra.
Here's the diff to add source map support.
Using GitHub Actions, we can configure automatic testing via .yml
files under .github/workflows.
The tsconfig.json
settings generate .js
built files, and .js.map
source map files, next to the original .ts
file. While some IDEs conveniently hide these files, it may be desirable to output them in a separate directory, typically dist
.
This can be done using the rootDir
/outDir
settings in tsconfig.json
, but that sort of setup comes with an annoying limitation of Typescript that forbids importing files outside the rootDir
. That can be a problem with monorepos.