Setting Up Typescript Project From Scratch

A woman sitting in an office chair pointing to a computer in front of a desk with a many bubbles floating on top of it

Project from scratch with Jest, Airbnb guidelines, and all

TL;DR

You don't want to read through all those steps? Dive in straight into coding, just clone/download/fork this repository with the resulting base project of this post 😀.

Introduction

Whenever learning a new language after configuring a proper development environment, set up a project from scratch is the next step on my list.

To understand how to have everything in the right place to start coding is essential. Of course, after the first few times, you will probably automate these steps or would rely on some boilerplate project.

The idea of this post is to walk through the very first steps necessary to have a project correctly set up for Typescript, and by correctly, I mean having these things in place:

  • Project structure: Folder structure, Git, NPM
  • Unit test setup: Jest
  • Style guide: Airbnb style guide for Typescript
  • NPM Scripts: Wrap up common commands in NPM scripts
  • Pre commit hooks: To ensure we don't tarnish our immaculate repository

This structure could be used for front-end development, probably with some tweaks here and there. But for my purpose and also for the post, it is oriented for backend development.

Also, everything here is aimed towards *nix environments, either be some Linux flavor or MacOs, with NodeJS and Git installed.

Setup

1 - Initiate the Project

Define the folder name that will house our project. In this case, let's call it ts-project.

mkdir -p ts-project/{src,tests/unit/src}

The above command will generate this structure:

ts-project
├── src
└── tests
    └── unit
        └── src

Hop into the project's folder.

cd ts-project

Initiate an empty Git Repository:

git init

Add a .gitignore file at the root of the project with the following content:

node_modules/
dist/

Which will tell Git to not track the changes on those folders.

Initiate an NPM project. The -y tells NPM to accept all the default settings:

npm init -y

Install Typescript:

npm install --save-dev typescript

Do not ignore the --save-dev flag. It tells NPM to add the Typescript package to the dev dependency list on our newly added package.json.

Initiate Typescript by issuing:

npx tsc --init

This line deserves a word or two. Alongside NPM, it is installed another tool called NPX. NPX is a tool to execute binaries without having them installed globally. It will look for the executable first at the environment variable $PATH, then in the local project for the requested command, in this case, tsc.

The tsc portion of the command refers to the Typescript dependency. When executed, the above command should display something like this as a result:

message TS6071: Successfully created a tsconfig.json file.

It creates a configuration file called tsconfig.json with parameters necessary for Typescript to work properly.

By default, all possible configuration keys are present, but most of them, will be commented out. After cleaning up the unnecessary commented lines you will be left with something like this:

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

For a detailed description of what each of these fields means, please check the official documentation right here.

Let's tweak this a little bit. Add two new keys to the compiler options

"outDir": "dist",
"sourceMap": true
  • outDir: being the destination folder of the transpiled code will be stored, in this case, we go with the most common of all, dist.
  • sourceMap: enables the generation of source map files. They allow debuggers and other tools to show the original Typescript when debugging the compiled Javascript.

And at the root we add:

"include": [ "./src/**/*" ]

Which tells the compiler to take everything from the source (src) folder.

The end result is something like this:

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "dist"
  },
  "include": [ "./src/**/*" ]
}

2 - Unit Tests

For unit tests, I have been using Jest for quite some time now, no complaints.

Very straight forward and simple testing framework.

To install all the packages necessary run:

npm install --save-dev \
  jest \
  babel-jest \
  @babel/core \
  @babel/preset-env \
  @babel/preset-typescript \
  @types/jest

Then add a babe.config.js at the root of the project with the content:

module.exports = {
  presets: [
    [ '@babel/preset-env', { targets: { node: 'current' } } ],
    '@babel/preset-typescript',
  ]
};

And our testing setup is done.

3 - Style guide and linting

This is a crucial step to ensure, among other things consistency. I am have been working with the Airbnb style guide for Javascript for almost two years, and love it. Helps to fix smaller mistakes literally pointing out to you.

To be able to use the same ruleset on Typescript, we are going to use a package called eslint-config-airbnb-typescript, which is a drop-in replacement for the normal eslint-config-airbnb but with all the Typescript goodness.

To install, run the following:

npm install --save-dev \
    eslint \
    eslint-config-airbnb-typescript \
    eslint-plugin-import@^2.22.0 \
    @typescript-eslint/eslint-plugin@^4.4.1

And add .eslintrc.js to the project root with the content:

module.exports = {
  extends: ['airbnb-typescript/base'],
  parserOptions: {
    project: './tsconfig.json',
  },
};

In case you went through my last post "Setting up Neovim for typescript development" you will notice that this style guide uses Eslint, and we configured the only tsserver. To add Coc support for Eslint run:

:CocInstall coc-eslint

Check out its documentation to learn more about the extension.

4 - NPM Scripts

Let's leverage the NPM scripts system to facilitate interacting with the tooling we just set up.

This seems like a trivial, maybe unnecessary step, but having the tooling abstracted by the scripts can help to decouple it from other parts like some editor shortcuts or CI/CD pipelines. So in case, you decide to change your testing library or build process, we can simply change it in one place.

Add this piece of code at the root of the package.json:

"scripts": {
    "test": "jest",
    "lint": "eslint",
    "compile": "tsc"
}

These are pretty self-explanatory, but here are examples of how we can use these scripts. From the project root run:

# This will run the testing library Jest
npm run test
# This will run the linting
npm run lint
# This will run the compilation
npm run compile

5 - Pre-Commit Hooks

Finishing up with some fail-safes, it can make our lifes much easier. Git hook is a neat feature from Git, it allows us to run scripts in certain key events like before applying a commit, before pushing, and many others.

In this example, we will use a package called pre-commit to run our scripts before the commits. To install it, run:

npm install --save-dev pre-commit

And then add this to package.json:

"pre-commit": [
    "test",
    "lint",
    "compile"
],

This will make sure that every time you issue a commit command, runs all the three npm scripts. That way, we will never be able to event commit broken or invalid code.

6 - Testing

With everything in place, let's write a "hello world" and test it. Add an index.ts to your src folder, located at the root of the project with this content:

/**
 * Hello world function
 *
 * @param {string} name
 */
function helloWorld(name: string) {
  return `Hello world, ${name}`;
}

export default helloWorld;

And add an index.spec.ts at tests/unit/src with this content;

import helloWorld from '../../../src/index';

test('Hello world works', () => {
    expect(helloWorld('Foo'))
        .toBe('Hello world, Foo');
});

Now, from the command line at the root of the project, run:

npm run lint && npm run test && npm run compile

Which should result in something like this:

> ts-project@1.0.0 lint /Users/username/ts-project
> eslint


> ts-project@1.0.0 test /Users/username/ts-project
> jest

 PASS  tests/unit/src/index.spec.ts
   Hello world works (2 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.898 s, estimated 1 s
Ran all test suites.

> ts-project@1.0.0 compile /Users/username/ts-project
> tsc

We've made it!

Conclusion

These kinds of steps are valuable to understand all the moving parts that compose a base project structure, but after a couple of times, these steps should be automated or a boilerplate that fits your needs created.

We went through all the steps to set up a solid foundation for our experiments and projects. Now the only thing left is to build something cool.