DEV Community

t7yang
t7yang

Posted on

TypeScript + Yarn Workspace Monorepo

When you have serveral highly coupled projects which you want to organize them together, you can consider a monorepo.

Yarn (1.x) provide the workspace feature to help you organize monorepo project.

Yarn workspace has serveral advantages like:

  • Hoist same dependecies to top level to avoid duplicate install.
  • Upgrade dependencies is much more easier.
  • Easy to run a same script for all projects.

A typical monorepo is a backend api and frontend SPA project.

This demo show only the steps to setup the shared part and the backend part.

The source code can find in https://github.com/t7yang/ts-yarn-workspace-demo

File Structure

.
├── backend
│   ├── package.json
│   ├── src
│   │   └── index.ts
├── shared
│   ├── package.json
│   ├── src
│   │   ├── index.ts
├── package.json
Enter fullscreen mode Exit fullscreen mode

How to

Setup yarn workspace in package.json

Yarn workspace must set private to true. In workspaces field you either explicitly list the path of projects or put all projects under a directory (said packages) then write as packages/* in workspaces field.

{
  "private": true,
  "workspaces": [
    "shared",
    "backend"
  ]
}
Enter fullscreen mode Exit fullscreen mode

Add shortcut for projects (optional)

You don't need cd into each project to run a script, simply run yarn workspace 'workspacename' 'scriptname' in project's root.

A shortcut can make this easier:

  ...
  "scripts": {
    "backend": "yarn workspace backend",
    "be": "yarn workspace backend",
    "shared": "yarn workspace shared"
  }
Enter fullscreen mode Exit fullscreen mode

Then you can run script in more shorter way, the scripts below is equivalance.

$ yarn workspace backend start
$ yarn backend start
$ yarn be start
Enter fullscreen mode Exit fullscreen mode

Add TypeScript dependency

Just like the scripts above, adding dependency to a workspace is:

$ yarn workspace shared add -D typescript
$ yarn workspace backend add -D typescript
Enter fullscreen mode Exit fullscreen mode

Run yarn install again in root project, yarn will hoist the same dependencies among sub projects to the top level node_modules.

Setting tsconfig.json

A share tsconfig.json can place in the root project:

{
  "compilerOptions": {
    /* Basic Options */
    "target": "es5",
    "module": "commonjs",
    "lib": ["ESNext"],

    /* Strict Type-Checking Options */
    "strict": true,

    /* Module Resolution Options */
    "moduleResolution": "node",
    "esModuleInterop": true,

    /* Advanced Options */
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true
  }
}
Enter fullscreen mode Exit fullscreen mode

For the shared sub project, composite: true field is required, other fields is optional but highly recommended, especially outDir and rootDir.

{
  "compilerOptions": {
    /* Basic Options */
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "composite": true,
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"]
}
Enter fullscreen mode Exit fullscreen mode

Due the transpiled code of shared sub project is dist, point the main and types fields in package.json to dist.

{
  ...
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  ...
}
Enter fullscreen mode Exit fullscreen mode

For the backend sub project, references field is required. The path field should point to the folder contain tsconfig.json or the tsconfig.json itself (here we point to the folder for short).

{
  "compilerOptions": {
    /* Basic Options */
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"],
  "references": [{ "path": "../shared" }]
}
Enter fullscreen mode Exit fullscreen mode

Start Coding

After the routine setup, now we can start coding. Add some code in shared/src, don't forget to export them to shared/src/index.ts

// shared/src/user.ts
export interface User {
  name: string;
  age: number;
}

export const createUser = (name: string, age: number): User => ({ name, age });

export const showUser = (user: User) => console.log(`${user.name} is ${user.age} years old.`);

// shared/src/index.ts
export * from './user';
Enter fullscreen mode Exit fullscreen mode

Import the code from shared in backend files, if everything is setup correctly, vscode will not complain and recognize the imports properly.

// backend/src/index.ts
import { createUser, showUser, User } from 'shared';

const user: User = createUser('t7yang', 18);

showUser(user);
Enter fullscreen mode Exit fullscreen mode

Add Script and Run

Finally, add the build and start script for backend.

{
  ...
  "scripts": {
    "build": "tsc --build",
    "start": "yarn build && node dist/index.js"
  },
  ...
}
Enter fullscreen mode Exit fullscreen mode

Run yarn backend start in root project, the backend project will transpile and run correctly.

$ yarn backend start
yarn run v1.22.4
$ yarn workspace backend start
$ yarn build && node dist/index.js
$ tsc --build
t7yang is 18 years old.
Enter fullscreen mode Exit fullscreen mode

You may curious why we don't have to build shared before we build backend.

tsc is smart enough to transpile the project which referenced by backend and transpile them if necessary before transpile itself.

Summary

By combining TypeScript project references and Yarn workpsace, we can setup a monorepo fast and easy.

If you need more powerful monorepo features, consider to use lerna or Yarn v2 Workspace.

References:

Top comments (16)

Collapse
 
ivawzh profile image
Ivan Wang

Thanks for the nice article. Wondering if you also have experience of implementing hot reload with TS composite approach? For the context, I am particularly interested at NextJS hot reload when shared module (aka package or workspace) updated. Thanks in advance.

Collapse
 
t7yang profile image
t7yang

I'm not working with NextJS before, but I don't think they going to support this.
github.com/vercel/next.js/issues/8708

Collapse
 
wiseintrovert_31 profile image
Wise Introvert

Great article. Can you please help me with this error: Could not find a declaration file for module '@my-organisation/common'. I followed your article to the teeth but can't seem to find a solution to this issue.

Collapse
 
t7yang profile image
t7yang

You can check the node_modules folder first to make sure @my-organisation/common existed. Somehow after yarn install / update some packages just "disappear". If so, you can do a yarn install --force to force yarn install again.

Collapse
 
aralroca profile image
Aral Roca

In my case I checked an exist inside the node_modules, but without the dist folder.

Image description

shared package tsconfig.json:

{
  "compilerOptions": {
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "composite": true,
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": [
    "src"
  ]
}
Enter fullscreen mode Exit fullscreen mode

and shared package.json:

{
  "name": "shared",
  "version": "1.0.0",
  "license": "MIT",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "files": [
    "dist"
  ],
  "devDependencies": {
    "typescript": "4.9.5"
  }
}
Enter fullscreen mode Exit fullscreen mode

Do you know why this can happen? Thanks!

Thread Thread
 
aralroca profile image
Aral Roca

@t7yang I updated your example from typescript 3.8.3 to 4.9.5 and is not working either. Do you know how to fix it for TypeScript v.4.9.5?

Thread Thread
 
t7yang profile image
t7yang

have you run the build cmd? you need to build at least once to generate the dist folder. the final solution is run the tsc in your shared folder.

Collapse
 
wiseintrovert profile image
wiseintrovert

Thanks!

Collapse
 
cyberwombat profile image
Yashua

For those that get an issue with Cannot find module "shared" or its corresponding type declarations when it is not yet built when you try to build the backend... I had this in my base config: "lib": ["es6", "es2015"],. Changing it to "lib": ["ESNext"], fixed the issue.

Collapse
 
shelob9 profile image
Josh Pollock

This is my go-to, quick guide to setting up TypeScript in a monorepo. Thank you

Collapse
 
nasht profile image
Nathan Hazout

Do you have insights on how to dockerize / deploy such a monorepo? How would you setup the docker files?

Collapse
 
t7yang profile image
t7yang

You can reference to this article xfor.medium.com/yarn-workspaces-an...

Collapse
 
marais profile image
Marais Rossouw

Great read man! Might also be worth mentioning here that projectReferences are a thing - helps speed up building too.

Collapse
 
dmytro_dmytro_dmytro profile image
Dmytro Dmytro

Sounds pro! Thanks. In the following article, I consider the same topic, but from the aliases issue perspective: webman.pro/blog/how-to-setup-types....

Collapse
 
ferlopezm94 profile image
Fernando López Martínez

Thanks for the article! Wondering why you didn't need to add the "shared" package as a dependency in the package.json of "backend"?

Collapse
 
t7yang profile image
t7yang

That because the "node module resolution rule".
node will keep finding upward until found or failed.