Skip to content

dandv/typescript-modern-project

Repository files navigation

Modern TypeScript project template

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.

Emit ES modules code

In tsconfig.json, set this in compilerOptions:

    "target": "esnext",
    "module": "esnext",  // Output `import`/`export` ES modules

Import modules that use Node built-ins (http, url etc.)

  • run npm install --save-dev @types/node
  • in tsconfig.json under compilerOptions, set
    • "moduleResolution": "node", so tsc can find modules when targeting ES6+
    • "types": ["node"] to avoid errors related to Node built-in modules

Import modules that don't have named exports

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 from package.json, and
  • changing the module line to "module": "CommonJS" in tsconfig.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.

Import your own modules without specifying an extension

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.

Optional chaining

To support optional chaining, add the --harmony flag to the node command line.

Run the resulting JavaScript code

Add "type": "module" to package.json, because TypeScript can't generate files with the .mjs extension.

ESLint

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.

Testing with Jest

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 the types array in tsconfig.json
  • add the 'jest' plugin to .eslintrc.cjs and also add 'jest/globals': true to its env key
  • use the jest.config.cjs generated by ts-jest config: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

Source maps

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.

CI testing

Using GitHub Actions, we can configure automatic testing via .yml files under .github/workflows.

TODO

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.

About

TypeScript+ESLint+Jest project template to import modules without extension, and modules with default exports that use node built-ins

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published