How to set up your first TypeScript project with TDD and CI (MacOS)

Introduction

In last week’s article we looked into how to create a basic portfolio website with HTML, CSS and JavaScript. Unsurprisingly this portfolio website was a front-end project, with no real backend. Today we will dive into the beginning of backend development with TypeScript and Node.

TypeScript, a superset of JavaScript, provides static type checking that helps catching errors early in the development process helping you to write clean, maintainable code. When combined with Test-Driven Development (TDD) and Continuous Integration (CI), you can ensure that your code is reliable, scalable, and easier to refactor.

This guide will walk you through setting up your first TypeScript project using TDD with Jest and CI with GitHub Actions on a MacOS system with Apple Silicon. We’ll cover everything from initializing your project to running your tests in a CI pipeline.

If you run into EACCES permission errors I recommend reading this article about how to solve it.

Table of Contents

  1. Setting Up Your Development Environment
  2. Initializing Your TypeScript Project
  3. Setting Up TDD with Jest
  4. Writing Your First Test
  5. Setting Up Continuous Integration with GitHub Actions
  6. Running Your CI Pipeline
  7. Conclusion
  8. Bonus: My Sample Project Setup

Setting Up Your Development Environment

Before we start, make sure you have the following tools installed on your MacOS system:

  • Node.js and npm
  • TypeScript
  • Git

Installing Node.js and npm

First, install Node.js, which includes npm (Node Package Manager). We recommend using a version manager like nvm (Node Version Manager) to manage Node.js versions efficiently.

Open your terminal and execute the following commands:

# If you don't have Homebrew installed start there
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# Once brew is installed move on to nvm
brew install nvm

# Add the following lines to your shell profile file (e.g., ~/.bashrc, ~/.zshrc, or ~/.bash_profile):
export NVM_DIR=~/.nvm
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"

# Restart your terminal and run this command
source ~/.your_shell_profile

# To install the latest version of node run this command
nvm install --lts

If you receive an error for EACCES probably you have to use the ‘sudo’ keyword (superuser do) before the command. This will prompt for you to enter your password. Type out your password and hit enter. It will look like you are not writing, just trust the process and hit enter at the end.

Installing TypeScript

Next, install TypeScript globally using npm.

# globally install to use typescript cli
npm install typescript -g

Installing Git

If you haven’t already installed Git, you can do so using Homebrew:

# Install Git
brew install git

# Verify installation
git --version

Initializing Your TypeScript Project

Create a new directory for your TypeScript project and initialize it with npm. Open your terminal, navigate to the parent directory of where your project will live, and execute the following commands:

# Create a new folder
mkdir my-typescript-project

# Change to that new folder
cd my-typescript-project

# Initialize node
npm init -y

This will create a package.json file in your project directory. Next, install TypeScript and initialize a TypeScript configuration file. Remember to use the ‘sudo’ keyword in front of your command if you run into EACCES errors.

# Install TypeScript locally in your project
npm install typescript --save-dev

# Initialize a TypeScript configuration file
npx tsc --init

# Create source (for ts files) and distribution (for js files) folders
mkdir src
mkdir dist

# Add script.ts
touch src/script.ts

The tsc --init command creates a tsconfig.json file with default settings. You can customize this file to suit your project’s needs.

# Initialize git version control
git init

# Add .gitignore
npx gitignore node

Setting Up TDD with Jest

Jest is a popular testing framework for JavaScript and TypeScript projects. We’ll use it to write and run our tests following the TDD methodology.

Installing Jest

First, install Jest and its TypeScript support packages. Using the –save-dev flag we indicate that these packages need to be installed as a development dependency.

npm install --save-dev typescript jest ts-jest @types/jest

If you get an error similar to this: ERESOLVE unable to resolve dependency tree:

Then probably it is due to a dependency conflict between the versions of jest and ts-jest you’re trying to install. For example ‘ts-jest@29.1.4‘ expects ‘jest@^29.0.0‘, but for some reason, it’s not able to find a compatible version. This is a common issue in the JavaScript ecosystem, especially when using the latest versions of packages or when the package maintainers haven’t yet synchronized their dependency versions.

# This flag tells npm to ignore peer dependency conflicts and install the packages anyway:
npm install --save-dev jest ts-jest @types/jest --legacy-peer-deps

Configuring Jest

Create a Jest configuration file to enable TypeScript support.

npx ts-jest config:init

Open your project in VS Code. If you need to set up VS Code or the code . command visit this link.

# Open your project in your code editor
code .
// In your jest.config.js file:
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
};

Adding Scripts to package.json

Add scripts to your package.json file to run Jest easily.

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "build": "tsc"
  }
}

Optional step: Update your tsconfig.json. As of writing this article my usual settings are as follow:

{
  "compilerOptions": {
    "target": "ES6",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "allowImportingTsExtensions": true,
    "noEmit": true,
  },
  "include": ["./src/**/*.ts"],
  "exclude": ["node_modules", "dist"]
}

Writing Your First Test

With Jest configured, let’s write our first test using the TDD approach. Create a directory for your tests and add a sample test file.

# go back to the project directory first
mkdir tests
touch tests/script.test.ts

Open tests/sample.test.ts in VS Code and add the following test:

// tests/script.test.ts
describe('Sample Test', () => {
  it('should return true', () => {
    expect(true).toBe(true);
  });
});

Run your test using the following command:

npm test

You should see the test pass successfully. This simple test ensures that your Jest setup is working correctly.

Setting Up Continuous Integration with GitHub Actions

Continuous Integration (CI) automates the process of running tests and other checks whenever you push code to your repository. GitHub Actions is a powerful CI tool that integrates seamlessly with GitHub.

Creating a GitHub Repository

If you haven’t already, create a new repository on GitHub for your project. Choosing the repository settings: If your project is for learning purposes or to showcase your knowledge select public. If this is for your million dollar app, definitely choose private. Initialize it without a README file (you can add this later) and push your existing repository (the one we just created above).

# First commit your existing changes to the local git repository
git add .
git commit -m "Initial commit"

# You can copy the exact lines from GitHub
git remote add origin https://github.com/your-username/my-typescript-project.git # Replace with your GitHub username and repository name
git branch -M main
git push -u origin main
  1. Create a Personal Access Token with the workflow scope:
    • Go to GitHub settings.
    • Click on Generate new token.
    • Provide a note to remind you what the token is for.
    • Select the expiration for the token.
    • Under Select scopes, check the box for workflow and any other scopes you might need.
    • Click on Generate token.
    • Copy the token. Make sure to store it safely as you won’t be able to see it again.
  2. Update your repository’s remote URL to use the new token:
    • Open your terminal.Navigate to your repository.Update the remote URL to use the new token
  3. Push your changes again
# Replace <TOKEN>, <USERNAME>, and <REPOSITORY> with your actual token, GitHub username, and repository name respectively.
# Example (obviously with fake token): git remote set-url origin https://dfh34209rggn0myfaketoken0vf82opi32fn@github.com/AndreaRethy/TypeScriptSetup.git
git remote set-url origin https://<TOKEN>@github.com/<USERNAME>/<REPOSITORY>.git

# Push changes
git push origin main

If the above steps are followed correctly, you should be able to push your changes to GitHub without encountering the refusing to allow a Personal Access Token to create or update workflow error.

Adding a GitHub Actions Workflow

Create a directory for your GitHub Actions workflows.

mkdir -p .github/workflows
touch .github/workflows/main.yml

Open .github/workflows/main.yml and add the following content:

name: Test
on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Install dependencies
      run: npm install
    - name: Run tests
      run: npm test

This configuration sets up a CI workflow that runs on every push and pull request to the main branch. It uses the latest Ubuntu runner, checks out your code, installs dependencies, and runs your tests.

Warning: YAML is similar to JSON, but instead of curly brackets {} it uses spaces and instead of brackets [] for arrays it uses dashes – If the indentation is not correct, it will throw errors.

Pushing Changes to GitHub

Commit your changes and push them to your GitHub repository.

git add .
# message for this specific commit
git commit -m "Set up TypeScript project with TDD and CI"
git push origin main

Running Your CI Pipeline

Once you’ve pushed your changes, navigate to the “Actions” tab of your GitHub repository. You should see your workflow running. If everything is set up correctly, the workflow should pass, indicating that your tests ran successfully in the CI environment.

Conclusion

Setting up a TypeScript project with TDD and CI can significantly improve your development workflow, ensuring that your code is reliable and maintainable. By following this guide, you’ve learned how to:

  • Set up your development environment on MacOS with Apple Silicon
  • Initialize a TypeScript project
  • Configure Jest for TDD
  • Write and run tests
  • Set up a CI pipeline with GitHub Actions

Bonus: My Sample Project Setup

I recommend following the above steps at least once, but for further projects here is an empty setup to speed up the kick off: Empty TypeScript Project. You can copy this project to your library, rename the folder and run these three commands:

# Install node packages from dev dependencies
npm install

# Initialize a new git repository
git init

# If you used git clone to copy the project:
git remote remove origin

After this you can continue with Setting Up Continuous Integration with GitHub Actions. YAML is already included in the example project so you can skip that step.

With these tools in place, you’re well-equipped to develop high-quality TypeScript applications. Happy coding!