Unleashing TypeScript performance in monorepos with TSX
Tags: typescript, monorepo, node, javascript
Published: 2023-07-28 10:41:57

In the software development world, TypeScript-based monorepo projects are increasingly popular. Yet, they come with a unique set of challenges, the most notable being the time-consuming process of TypeScript compilation during development. 

At [Contember](https://www.contember.com/), we've faced this challenge head-on. Our solution is a combination of the TypeScript Execute tool (`tsx`) and Node.js' `--conditions` option. This approach allows us to run TypeScript source directly in Node.js, significantly accelerating the development process by eliminating the need for continuous recompilation.

In this article, we'll walk you through how we implemented this efficient strategy, enhancing the speed and overall quality of our TypeScript development in a monorepo environment. Let's dive in!

## The problem

### Monorepos

Monorepos, or monolithic repositories, are a codebase management strategy where the code for many projects or packages is stored in the same repository. This structure can bring numerous advantages, such as simplified dependency management, code sharing across projects, and unified versioning. However, when working with TypeScript within a monorepo, the continuous compilation process can slow down the development workflow.

### TypeScript

TypeScript is a statically-typed superset of JavaScript that enhances the language with type safety and other modern features. While TypeScript brings many advantages, it adds an extra step to the workflow: it must be compiled into JavaScript before it can be run in Node.js.

In a large project or a monorepo with multiple interdependent packages, waiting for TypeScript compilation can become a significant bottleneck in the development process. To streamline this workflow, we need a way to run TypeScript source code directly in Node.js without the intermediate compilation step. 

##  The tools
### tsx

[**TypeScript Execute (tsx)**](https://github.com/esbuild-kit/tsx) is a powerful command-line tool that enhances Node.js to run TypeScript and ESM files directly. It leverages the `esbuild` compiler to provide an insanely fast on-demand TypeScript and ESM compilation.

To add `tsx` to your project, install it as a dev dependency:

```
yarn add --dev tsx
```

The tsx designed to replace the `node` command for running TypeScript and ESM in both `commonjs` and `module` package types. For instance, to run a TypeScript file, you would simply use the command `tsx ./file.ts`.



### Node.js' `--conditions` Option

The [`--conditions` option](https://nodejs.org/api/cli.html#-c-condition---conditionscondition) is a feature in Node.js that allows you to specify custom conditions for resolving exports. It works in conjunction with the ["exports"](https://nodejs.org/api/packages.html#conditional-exports) field in your package's `package.json` file, giving you control over how module exports are resolved based on certain conditions.

For instance, by defining conditional exports in your `package.json` file, you could specify different entry points for your package based on whether it's being imported using CommonJS or ESM syntax, or whether it's being run in a `development` or `production` environment. 

By running your Node.js program with the `--conditions` flag, such as `node --conditions=development app.js`, you can enable custom resolution conditions for your module exports.

Combined, these two tools can dramatically improve your workflow in a TypeScript-based monorepo by allowing you to run TypeScript source code directly in Node.js, bypassing the need for continuous TypeScript compilation. Let's move on to explore how to implement this strategy.

## The Solution

### Configuring conditional exports in your monorepo

The first step is to set up conditional exports in your `package.json` file for each package in your monorepo. This configuration specifies the entry point that Node.js should use based on the given condition. For our purpose, we'll create a "typescript" condition that points to the TypeScript source files. You should also define a default export with a compiled source:

```json
{
  "exports": {
    "typescript": "./src/index.ts",
    "default": "./dist/src/index.js"
  }
}
```

In this setup, Node.js will import the TypeScript source files directly when the `typescript` condition is active. In other scenarios, it will default to importing the compiled JavaScript files from the `dist` directory.

### Running TypeScript directly using `tsx` and `conditions`

Once your conditional exports are set up, you can use the `tsx` tool along with the `--conditions=typescript` option to run your TypeScript source code directly in Node.js:

```sh
yarn tsx --conditions=typescript ./src/run.ts
```

This command activates the `typescript` condition. As a result, when Node.js resolves imports from your packages within the monorepo, it uses the TypeScript source files specified by the "typescript" conditional export. This eliminates the need for continuous TypeScript compilation, allowing for a smoother and faster development cycle.

And that's it! You've now configured your TypeScript-based monorepo to run TypeScript source code directly in Node.js. In the next section, we'll discuss some practical examples where this solution is beneficial.

### Bonus: watch mode with TSX

A great advantage of using TSX in your TypeScript monorepo is its watch mode feature. With this, TSX not only allows you to execute TypeScript source code directly in Node.js, but it can also watch for changes in your TypeScript files and automatically restart your application whenever modifications are detected.

This can be particularly useful during the development phase of your project, where changes are frequent and real-time feedback is crucial for a smooth and efficient workflow.

You can activate the watch mode by running the following command:

```sh
tsx watch --conditions=typescript ./file.ts
```

With this command, TSX will monitor `./file.ts` and its dependencies. When any of them change, TSX will rerun the script with the `typescript` condition. This means your Node.js server gets restarted automatically whenever you make changes to any of the TypeScript source files in your dependencies, giving you immediate feedback and a dynamic development experience.

## Conclusion

In TypeScript-based monorepos, the overhead of continuous TypeScript compilation can slow down development. By combining the TypeScript Execute (`tsx`) tool with Node.js' `--conditions` option, we have been able to run TypeScript source code directly when developing our [Contember platform](https://github.com/contember). This approach has not only reduced the turnaround time for changes but also streamlined the integration of monorepo packages, greatly enhancing our development workflow.