Starting a TypeScript Project in 2021

This is a guide for starting a TypeScript project in 2021 with modern tooling.

You can use the example repository (instead of the manual setup in this guide):

git clone https://github.com/metachris/typescript-boilerplate.git

Contents#


Basic project setup#

The basic setup consists of four steps:

  1. Create the project and source directories
  2. Create a package.json
  3. Get a .gitignore, tsconfig.json, .eslintrc.js
  4. Install TypeScript & dependencies

Note: This guide uses yarn, but if you prefer npm it has similar commands.

# Create project folder
mkdir my-project
cd my-project

# Create source folder and files
mkdir src
touch src/main.ts src/main.test.ts src/cli.ts

# Create a package.json
yarn init

# Install TypeScript, linter and Jest
yarn add -D typescript @types/node ts-node
yarn add -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
yarn add -D jest ts-jest @types/jest

# Get a .gitignore
wget https://raw.githubusercontent.com/metachris/typescript-boilerplate/master/.gitignore

# Get a tsconfig.json with some defaults (adapt as needed)
wget https://raw.githubusercontent.com/metachris/typescript-boilerplate/master/tsconfig.json

# Alternatively you can create a fresh tsconfig.json (with extensive docstrings)
tsc --init

# Get a .eslintrc.js
wget https://raw.githubusercontent.com/metachris/typescript-boilerplate/master/.eslintrc.js

# Get a jest.config.json, for ts-jest to run the tests without a separate typescript compile step
wget https://raw.githubusercontent.com/metachris/typescript-boilerplate/master/jest.config.js

# Create a git repo and make the first commit
git init
git add .
git commit -am "initial commit"

Use src/cli.ts for code that’s run from the command line. This way code from main.ts can be included without running the entrypoint code, and allows for easier cross-target building and code branches (eg. Node.js and browsers).

Add scripts to your package.json:

{
    "scripts": {
        "cli": "ts-node src/cli.ts",
        "test": "jest",
        "lint": "eslint src/ --ext .js,.jsx,.ts,.tsx",
        "build": "tsc -p tsconfig.json",
        "clean": "rm -rf dist build",
        "ts-node": "ts-node"
    }
}

Now you can run yarn cli, yarn test, yarn lint, yarn build and yarn ts-node <filename>.

💡 In Visual Studio Code you can use the build and test tasks to start scripts with keyboard shortcuts. In the command palette “Configure Default Build Task” and “Configure Default Test Task” (see the VS Code docs).


Tests with Jest#

You can write Jest tests like this:

import { greet } from './main'

test('the data is peanut butter', () => {
  expect(1).toBe(1)
});

test('greeting', () => {
  expect(greet('Foo')).toBe('Hello Foo')
});

Run the tests with yarn test, no separate compile step is necessary.


esbuild#

esbuild is an extremely fast JavaScript bundler that can also compile a large subset of TypeScript code. You can use it to bundle both for Node.js as well as for browsers. esbuild is still relatively young and under active / heavy development; see also esbuild on GitHub.

Why use esbuild in addition to tsc? The TypeScript compiler doesn’t bundle well for browsers (developers usually resort to additional bundlers like webpack, parcel or rollup), and it’s pretty slow.

Install esbuild:

yarn add -D esbuild

Bundling for Node.js#

Additionally to tsc (the TypeScript compiler), you can also bundle code with esbuild for Node.js like this:

# Compile and bundle
yarn esbuild src/cli.ts --bundle --platform=node --outfile=dist/esbuild/cli.js

# Same, but minify and sourcemaps
yarn esbuild src/cli.ts --bundle --platform=node --minify --sourcemap=external --outfile=dist/esbuild/cli.js

# Run the bundled output
node dist/esbuild/cli.js

Read more about the esbuild options in the esbuild documentation.

Notes:

  • When building with esbuild, you can use the --watch option to rebuild whenever a file changed
  • esbuild does currently not support building .d.ts declaration files (see also this issue). You need to build those with tsc.
  • The example repository includes the esbuild commands as scripts in package.json

Building a browser-compatible module#

You can generate browser compatible modules with bundlers such as esbuild, webpack, parcel and others.

This guide uses esbuild:

# Bundle for browsers
yarn esbuild src/browser.ts --bundle --outfile=dist/esbuild/browser.js

# Same, but with minification and sourcemaps
yarn esbuild src/browser.ts --bundle --minify --sourcemap=external --outfile=dist/esbuild/browser.js

The code in browser.ts will be executed once loaded in the browser.

esbuild has a --global-name=xyz flag, to store the exports from the entry point in a global variable. See also the esbuild “Global name” docs.

Accessing DOM properties (window, document)

You can access window and document in your code when loaded in a browser. You might want to use this to attach parts of your code to the window object.

In tsconfig.json, add DOM to the list of libraries:

"lib": ["ES6", "DOM"]

Create src/browser.ts as entrypoint for browser builds. There you can attach custom properties to window like this:

// Import a function
import { greet } from './main'

// Make it accessible on the window object
(window as any).greet = greet

Now bundle with esbuild:

yarn esbuild src/browser.ts --bundle --outfile=dist/esbuild/browser.js

Test the result with a simple website like this: browser-test.html


Publishing to npm#

Let’s publish the latest code to npm, for use with both Node.js and the browser.

npm and yarn ignore the files from .gitignore. Since dist is in there, we need to overwrite the npm ignore settings using a custom .npmignore:

wget https://raw.githubusercontent.com/metachris/micropython-ctl/master/.npmignore

Create a build and run yarn publish:

# Build with tsc and esbuild
yarn build-all

# Update the version and publish to npm
yarn publish

build-all builds the project with both tsc to get the type definiton files, and esbuild to build for Node.js and browsers.

After running yarn publish the project/new version is live on npm. 🎉

For example the npm package for this boilerplate project:


Using from Node.js#

You can install the module with npm:

npm install typescript-boilerplate-2021

# or with yarn
yarn add typescript-boilerplate-2021

Using the module in custom code:

import { greet } from 'typescript-boilerplate-2021'

greet("World")

Using from the browser#

There are several CDNs which automatically deliver npm projects, like jsDelivr, cdnjs, unpkg.com or skypack.

Without any manual intervention, you can access packages on jsDelivr like this:

You can reference the bundle from HTML like this:

<script src="https://cdn.jsdelivr.net/npm/typescript-boilerplate-2021@0.3.0"></script>

Test the result with a simple website like this: browser-test.html


Continuous integration#

You probably want to run the tests and linter on every code push. Additionaly you might want to build and deploy the docs from CI too.

GitHub Actions#

See GitHub Actions docs. Create a file .github/workflows/lint-and-test.yml:

name: Lint and test

on: [push, pull_request]

jobs:
  lint_and_test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        nodejs: [10, 12, 14]

    steps:
    - uses: actions/checkout@v2

    # https://github.com/actions/setup-node
    - uses: actions/setup-node@v2-beta
      with:
        node-version: ${{ matrix.nodejs }}

    - run: yarn install
    - run: yarn test
    - run: yarn lint
    - run: yarn build-all

Testing in multiple operating systems

If you want to verify your build / package on various operating systems (Windows, Linux, macOS), you can setup a matrix for a job like this:

jobs:
  default-version:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [macos-latest, windows-latest, ubuntu-latest]

    steps:
    - uses: actions/checkout@v2
    - uses: actions/setup-node@v2-beta
      with:
        node-version: 12
    ...

GitLab CI#

See GitLab CI docs. Create a file .gitlab-ci.yml:

image: node:12

cache:
  paths:
    - node_modules/

stages:
  - test

lint-and-test:
  stage: test
  script:
    - yarn install
    - yarn test
    - yarn lint
    - yarn build-all

API documentation with TypeDoc#

You can auto-generate API documentation from the TypeScript source files using TypeDoc, which builds on JSDoc syntax. The generated documentation can be published to GitHub / GitLab pages through the CI.

  • Install TypeDoc: yarn add -D typedoc
  • Add docs script to package.json: "typedoc --entryPoints src/main.ts"

Documentation strings look like this:

/**
 * This comment _supports_ [Markdown](https://marked.js.org/)
 */
export class DocumentMe {}

Generate the documentation with yarn docs. The resulting HTML is saved in docs/.

You can use CI to automatically publish the docs to GitHub or GitLab pages:

For example this the documentation for the example project: https://metachris.github.io/typescript-boilerplate/


Summary#

This post covered a full TypeScript project setup, with tests, esbuild, bundling for Node.js and browsers, publishing on npm, continuous integration and automatic documentation.

You can use the boilerplate repository like this:

git clone https://github.com/metachris/typescript-boilerplate.git
cd typescript-boilerplate

yarn install
yarn test
yarn lint
yarn build-all

References#


Notes#

  • You might be interested in using Deno instead of Node.js. Deno is a modern V8 runtime with a lot of great features, created by Ryan Dahl, author of Node.js. It’s still somewhat new and experimental though.
  • To bootstrap a full web project, you might want to use hot module replacement (HMR). This is outside the scope of this post. In the meantime, see the webpack docs and parcel docs about HMR.
  • If you want to target both Node.js and browsers, be aware that several APIs are different, most notably fetch, WebSocket, Buffer.
  • See also https://github.com/formium/tsdx for a more fully featured TypeScript project boilerplate

Reach out with feedback and ideas:

Join the discussion of this post on Hacker News.