Typescript: Working with Paths, Packages and Yarn Workspaces

How to modularise your Typescript projects with paths, packages and Monorepos

We are all accustomed to working with packages as Javascript developers, installing and importing various dependencies that our project requires, from the framework itself to fine-tuned UI.

This article explores critical tools and concepts a developer can leverage as a means of managing your codebase to promote modularity and reusability, and overall aid in project management. In particular, we will cover:

  • Typescript paths aliases and how they simplify your project structure and import statements. We’ll also cover the fallbacks of using paths in terms of their support in major frameworks today
  • Alternatives to using paths with your private package registry, and where this would be suited. We will cover the process of configuring a Typescript package, and deploying it to an NPM registry.
  • Multi Package Projects / Monorepos: We’ll cover how a monorepo project is set up in Typescript and the benefits they bring. A monorepo is a project hosted as a repository, that contains a multitude of separate packages that make the complete project. For example, you may have a @myapp/ui as a package to handle the app’s common UI elements, @myapp/types to define all the types used, and @myapp/styles to define the CSS / styled components used throughout your monorepo. This is a powerful concept for a few reasons — they will be covered further down
  • Yarn workspaces: Yarn workspaces provide a built-in means of configuring a monorepo. We’ll cover how to set up a Typescript-based monorepo, and introduce a dedicated tool, Lerna, for multi-package projects

With these concepts under your belt, you can introduce them to your existing and future projects where they make sense — monorepos for example are great for larger projects, but perhaps not as suited for a small-scale personal blogs. Path aliases on the other hand could work very well with smaller projects, and also utilise in-house packages for re-usable components.

Using Path Aliases

Typescript paths, also known as path aliases, allow us to define custom paths to a module, simplifying import statements. They are configured in tsconfig.json.

Paths bring two big benefits:

  • They provide simplified paths to directories, allowing them to act as shortcuts to packages and other resources used throughout your project
  • They vastly decrease the use of relative import statements and the amount of ../'s, a welcome optimisation of projects with deep nesting.

Instead of importing a module from somewhere like a top-level utils folder, we can instead define a path, called @utils, and use it in my import statements:

// using a path
import { func } from '@utils/common';
// not using paths
import { func } from '../../../../utils/common'

Import statements now become a lot easier to read and manage. Defining paths is done under a paths block inside tsconfig.json:

"baseUrl": "./src",
"paths": {
"@utils/*": [

Notice that our @utils path can point to a number of directories; we provide an array of paths that the module could be sitting in. Also note that a baseUrl is defined as the project’s src/ folder. This is the relative location to which paths originate.

With this in mind, our project structure may resemble the following:


The baseUrl within tsconfig.json is pointing to the src/ folder, which houses a packages/ directory with our util scripts therein.

Note: Paths can lead to anywhere under the baseUrl; you are not limited to a packages/ directory, although a packages/ directory is a common convention, especially if projects are (or plan) to move to a multi-package setup.

Path Alias Support

Depending on which framework you are using, Typescript path aliases may not be supported, leading to problems with either the development build or the Javascript production build.

NodeJS environments are a great use-case for path aliases today, whereas frameworks like Create React App is still working to support them. Let’s briefly explore compatibility with popular environments.

Note: In the context of front-end apps, you also have the option of configuring a project from scratch to support path aliases, React or otherwise, but that is out of the scope of this article.

The issue with NodeJS projects is that there is no means for the compiled Javascript code to recognise Typescript path aliases. To get paths working with Node, we can install another package, module-alias. Designed for NodeJS, this package creates aliases of directories, and registers custom module paths in vanilla Javascript within a package.json interface.

To set this up, simply install the package with yarn:

yarn add module-alias

Within package.json, add a _moduleAliases block. This block closely resembles the tsconfig paths block:

"_moduleAliases": {
"@utils": "dist/packages/utils"

Notice that dist/ is included in the path location; this is the folder where the compiled Javascript is located. Finally, import a register function to your top level file, such as app.js if you’re using Express boilerplate for example:

import 'module-alias/register';

This is all that is needed to support paths in Node projects.

Note: module-alias is designed to work with final projects such as a web server or application, not with packages designed to be dependencies.

Typescript Angular projects support paths. Like our example project above, all that is needed is to amend tsconfig.json, this time paths and baseUrl residing inside compilerOptions:

"compilerOptions": {
"baseUrl": "src",
"paths": {
"@utils/*": ["src/packages/utils/*"],
"@environments/*": ["environments/*"],

CRA version 2 does not support path aliases. As a matter of fact, it automatically removes the baseUrl and paths options from the tsconfig.json any time we run its scripts.

Create React App 3.0 (Github release) now natively supports absolute imports with the baseUrl setting inside tsconfig.json. However, paths are still not supported at the time of writing, and are removed from tsconfig.json at runtime if any are defined. Support is being worked on, the progress of which is still an open issue.

Note: There is a somewhat verbose workaround for enabling paths for CRA described here.

Nevertheless, you may wish to upgrade to CRA3.0 if you have not done so already, and prepare your projects for the upcoming support of paths. If you are running a previous version of the package, firstly upgrade the global command-line utility, then update your projects react-scripts package and its dependencies:

# re-install Create React App
npm uninstall -g create-react-app
npm install -g create-react-app
# update existing project react-scripts
yarn add --exact react-scripts@latest
# upgrade your dependencies to ensure compatibility
yarn upgrade

Note: CRA version 3 does have breaking changes from the previous version. Most notably, the rules of hooks are now enforced, Typescript is now linted, and Jest 24 is now used. Check out all breaking changes here before upgrading.

Alternatives to Path Aliases

Paths are very useful for resolving modules at a single project level, but when those modules could be re-used in other projects, a better solution would be to create a package to be hosted on a private npm registry.

I have written an article to do exactly that, which can be found here:

Like path aliases, packages have the benefit of absolute import statements. There are a number of opportunities where packages can be utilised to tidy up your project, and limit code-repetition in other projects as a result, including:

  • Common UX components that could be generic
  • Styling boilerplate such as page layout, text styling and default styles
  • Common form validations that may be specific to your needs
  • API handlers and other wrappers to handle external services
  • Any emerging design patterns in your app that can be templated

Let’s next explore how we can package up a Typescript module and deploy it to an NPM registry.

Publishing Typescript Packages to NPM

Whether you have some code in an existing project to refactor, or if you wish to create a new package from scratch, the publishing process is relatively straight forward.

The process starts from initialising a new Typescript project and configuring the tsconfig.json file to output a .d.ts definition file. Let’s call the project my-ts-package:

mkdir my-ts-package
cd my-ts-package
# generate package.json
npm init -y
# generate tsconfig.json
tsc --init
# initiate git
git init

Note: Your output project folder, dist/ in this case, should be ignored in .gitignore.

Initiating Git is optional, but is recommended practice to leverage the benefits of managing and updating the package’s code.

Now, within tsconfig.json, we need to ensure that a definition file is generated at compile time, and ensure that a build directory is present:

// tsconfig.json{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"lib": [
"declaration": true,
"outDir": "dist",
"strict": true,
"esModuleInterop": true
"exclude": [


The declaration property declares that we wish to generate a .d.ts file when the project is compiled. In addition, make sure outDir is defined; in the above case the compiled Javascript will be saved in the dist/ folder.

Also, within package.json, a types property needs to be included, defining the location of the package’s definitions file that the Typescript compiler refers to when our package is imported into another project. This value is usually the same name as your package’s main entry point, in this case, index:

// package.json{
"name": "my-ts-package",
"version": "1.0.0",
"description": "My Typescript NPM Package",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc"
"author": "Ross Bulat",
"license": "MIT",
"devDependencies": {
"typescript": "^3.5.0"

From here, populate the package with your module’s exports, including its functions and types, and finally build it.

Note: Make sure the types defined in your project are exportable to give apps using the package access. Prepend export to any that are not.

# build package
yarn build

Now run npm publish to publish the package. Remember to include your private registry using the --registry flag to privately host your package if you indeed wish to do so. Leaving --registry blank will result in your package being uploaded to the public npm registry.

yarn publish --registry 'http://your-registry'

You are now able to install and add the package as a dependency to your projects:

# install package
yarn add my-ts-package --registry 'http://your-registry'
# verify package
yarn info my-ts-package

Now simply use your package’s exports as you would any other:

// somefile.tsximport { MyFunc } from 'my-ts-package';

With that, we have now walked through the process of setting up and publishing Typescript packages, giving you another tool for simplifying your codebase and project structure, while cleaning up your import statements in the process.

There are some scenarios however where separating your app functionalities via packages won’t make sense — that brings us to the multi-package / monorepo project structure, a more suited setup for larger projects where compartmentalising functionalities, components or sections of the app can aid in project upkeep and efficiency amongst teams of developers.

Perhaps you wish to separate entire sections of your app into different workspaces, such as a landing page app, user dashboard app and support app — and share their dependencies. Furthermore, common components used throughout these packages would also need to be separated into their own package.

These packages will need to be housed within a main project repository; a repository that brings them all together.

What we end up with is something like the following:

main project repo
each package has its own package.json and tsconfig.json

In this example, the apps package will embed the three sub-sections that will comprise the entire app, with each of those utilising the components from the ui- packages. Each package is dependent on another package.

This type of project is called a Monorepo. Let’s see how these can be configured, with Yarn Workspaces.

Working with Yarn Workspaces

Yarn workspaces are relatively straight forward to set up with some initial boilerplate. The aim of this section is to cover the basics of this setup, and point the reader in the right directions to expand on this initial setup.

With yarn workspaces, all package dependencies are installed in one command — yarn install — from the root package. Because of this, the most up to date version of a dependency is installed for all your packages. Also, only one yarn.lock file is generated to prevent conflicts between them.

Note: Where package specific dependencies are required, e.g. if one package requires a different version of a dependency than another package, then it will be saved at that package level.

To enable yarn workspaces, add a workspaces configuration in the root folder’s package.json, and ensure your package is set to private:

"private": true,
"workspaces": [

As a security precaution, workspaces must be private. The workspaces property itself takes an array of directories, and supports the wildcard. In this case we have defined every folder under the packages/ directory to be a workspace.

The folder structure of our workspaces commonly resemble the following:



Let’s break down this setup:

  • The root apps/ folder houses all the packages under its packages/ folder, and contains the top level configuration of the project via the package.json and tsconfig.json files. Your .gitignore and Readme files can also live at this top level, but it is encouraged to write a separate Readme for each package too.
  • Each package is housed in a containing folder, which are commonly named identically to the name field in their package.json. In our case, the app package is loaded to run the app.

Note: In the case a package itself initiates your app, you can amend your start script in the root’s package.json file to run that particular app:

"scripts": {
"start": "cd packages/app && yarn start",

This allows you to run yarn start in your root directory while running the correct package to start the app.

  • Each package has its own tsconfig.json, which extends the root package’s tsconfig.json to save repetition and keep consistency. However, each package needs its own rootDir and outDir values. The following resembles a valid configuration file:
// packages/app/tsconfig.json{
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "es5",
"module": "esnext",
"moduleResolution": "node",
"rootDir": "src",
"outDir": "dist",

"declaration": true

One workspace can depend on another workspace simply by defining dependencies in the respected package.json file, just like you would with any other dependency:

// packages/app/package.json{
"dependencies": {
"ui-app": "1.0.0",
"ui-styles": 1.0.0"

Once your configuration is complete, simply run yarn install from the root directory, and all your packages will be up to date, and ready to run.

Yarn workspaces was not designed to be an all-in-one solution for multi-package management. Instead, it solves the problem of simply acknowledging such a concept, with its efficient dependency management across multiple packages. To extend the capabilities of the monorepo, we have dedicated tools, the most popular of which is Lerna.

Yarn workspaces is commonly used in conjunction with Lerna, a tool specifically used for multi-package projects. Lerna was released before Yarn Workspaces, however it quickly enabled support for the feature, and now acts more as a companion than a competitor. In fact, Lerna is still proving to be a vital tool for the Javascript community, currently on over 430k weekly downloads at the time of this writing. Lerna’s versioning and publishing tools are particularly useful to use with yarn workspaces.

Integrating Lerna into a project just requires the installation of the package, and addition of a small configuration file:

# add lerna as a dependencyyarn add lerna

The configuration, lerna.json, is typically saved next to package.json:

// lerna configuration{
"lerna": "3.14.0",
"packages": ["packages/*"],
"version": "1.0.0",
"npmClient": "yarn",
"useWorkspaces": true

Note that we are specifying the npmClient to be yarn, and that useWorkspaces is set to true, so Lerna knows to implement Yarn Workspace features instead of its native implementation. Like package.json, we are providing the package directories, this time with the packages property.

From here, check out the Getting Started section on Lerna’s Github.

Note: Deep diving into Lerna is out the scope of this article, but I will plug future articles here to expand on what we have introduced in this talk.


This article has explored various ways to improve your Typescript project management, through modularising and refactoring your codebase to be more efficient and re-usable. In summary:

  • Typescript path aliases are useful for shortening import statements, and reaching modules from deep nested components. Paths can be easily implemented in Node and Angular projects, but are currently lacking support with Create React App — although support is on the roadmap.
  • Deploying your own Typescript packages, either publicly or privately via your own registry, is an effective means of reusing your code and decreasing the size of your final projects. As a result of using absolute imports, your import statements are shortened by using packages. This is a nice benefit, but shouldn’t be the primary means of using packages!
  • Where projects require multiple packages, yarn workspaces can be leveraged, and used in conjunction with Lerna. This makes more sense for larger projects, or where compartmentalising your components and sections of your apps can streamline maintenance of the app.

The solutions documented here may not all be suited for one particular project; it is instead advised to adopt them where they make sense.

Programmer and Author. Director @ JKRBInvestments.com. Creator of LearnChineseGrammar.com for iOS and Android.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store