Typescript Project Linking

The naive way to reference code in a separate project is to use a relative path in the import statement.

1import { someFunction } from '../../teamA/otherProject'; 2 3const result = someFunction(); 4

The problem with this approach is that all your import statements become tied to your folder structure. Developers need to know the full path to any project from which they want to import code. Also, if otherProject ever moves locations, there will be superfluous code changes across the entire repository.

A more ergonomic solution is to reference your local projects as if they were external npm packages and then use a project linking mechanism to automatically resolve the project file path behind the scenes.

1import { someFunction } from '@myorg/otherProject'; 2 3const result = someFunction(); 4

There are two different methods that Nx supports for linking TypeScript projects: package manager workspaces and TypeScript path aliases. Project linking with TS path aliases was available with Nx before package managers offered a workspaces project linking approach. The Nx Team has since added full support for workspaces because (1) it has become more common across the TypeScript ecosystem and (2) using workspaces allows Nx to also enable TypeScript project references for improved build performance.

Project Linking with Workspaces

To create a new Nx workspace that links projects with package manager workspaces, use the --workspaces flag.

โฏ

npx create-nx-workspace --workspaces

Set Up Package Manager Workspaces

The configuration for package manager workspaces varies based on which package manager you're using.

package.json
1{ 2 "workspaces": ["apps/**", "packages/**"] 3} 4

Defining the workspaces property in the root package.json file lets npm know to look for other package.json files in the specified folders. With this configuration in place, all the dependencies for the individual projects will be installed in the root node_modules folder when npm install is run in the root folder. Also, the projects themselves will be linked in the root node_modules folder to be accessed as if they were npm packages.

Set Up TypeScript Project References

With workspaces enabled, you can also configure TypeScript project references to speed up your build and typecheck tasks.

The root tsconfig.base.json should contain a compilerOptions property and no other properties. compilerOptions.composite and compilerOptions.declaration should be set to true. compilerOptions.paths should not be set.

tsconfig.base.json
1{ 2 "compilerOptions": { 3 // Required compiler options 4 "composite": true, 5 "declaration": true 6 // Other options... 7 } 8} 9

The root tsconfig.json file should extend tsconfig.base.json and not include any files. It needs to have references for every project in the repository so that editor tooling works correctly.

tsconfig.json
1{ 2 "extends": "./tsconfig.base.json", 3 "files": [], // intentionally empty 4 "references": [ 5 // UPDATED BY PROJECT GENERATORS 6 // All projects in the repository 7 ] 8} 9

Individual Project TypeScript Configuration

Each project's tsconfig.json file should extend the tsconfig.base.json file and list references to the project's dependencies.

packages/cart/tsconfig.json
1{ 2 "extends": "../../tsconfig.base.json", 3 "files": [], // intentionally empty 4 "references": [ 5 // UPDATED BY NX SYNC 6 // All project dependencies 7 { 8 "path": "../utils" 9 }, 10 // This project's other tsconfig.*.json files 11 { 12 "path": "./tsconfig.lib.json" 13 }, 14 { 15 "path": "./tsconfig.spec.json" 16 } 17 ] 18} 19

Each project's tsconfig.lib.json file extends the project's tsconfig.json file and adds references to the tsconfig.lib.json files of project dependencies.

packages/cart/tsconfig.lib.json
1{ 2 "extends": "./tsconfig.json", 3 "compilerOptions": { 4 // Any overrides 5 }, 6 "include": ["src/**/*.ts"], 7 "exclude": [ 8 // exclude config and test files 9 ], 10 "references": [ 11 // UPDATED BY NX SYNC 12 // tsconfig.lib.json files for project dependencies 13 { 14 "path": "../utils/tsconfig.lib.json" 15 } 16 ] 17} 18

The project's tsconfig.spec.json does not need to reference project dependencies.

packages/cart/tsconfig.spec.json
1{ 2 "extends": "./tsconfig.json", 3 "compilerOptions": { 4 // Any overrides 5 }, 6 "include": [ 7 // test files 8 ], 9 "references": [ 10 // tsconfig.lib.json for this project 11 { 12 "path": "./tsconfig.lib.json" 13 } 14 ] 15} 16

TypeScript Project References Performance Benefits

Using TypeScript project references improves both the speed and memory usage of build and typecheck tasks. The repository below contains benchmarks showing the difference between running typecheck with and without using TypeScript project references.

TypeScript Project References Benchmark/jaysoo/typecheck-timings

Here are the baseline typecheck task performance results.

1Typecheck without using project references: 186 seconds, max memory 6.14 GB 2

Using project references allows the TypeScript compiler to individually check the types for each project and store the results of that calculation in a .tsbuildinfo file for later use. Because of this, the TypeScript compiler does not need to load the entire codebase into memory at the same time, which you can see from the decreased memory usage on the first run with project references enabled.

1Typecheck with project references first run: 175 seconds, max memory 945 MB 2

Once the .tsbuildinfo files have been created, subsequent runs will be much faster.

1Typecheck with all `.tsbuildinfo` files created: 25 seconds, max memory 429 MB 2

Even if some projects have been updated and individual projects need to be type checked again, the TypeScript compiler can still use the cached .tsbuildinfo files for any projects that were not affected. This is very similar to the way Nx's caching and affected features work.

1Typecheck (1 pkg updated): 36.33 seconds, max memory 655.14 MB 2Typecheck (5 pkg updated): 48.21 seconds, max memory 702.96 MB 3Typecheck (25 pkg updated): 65.25 seconds, max memory 666.78 MB 4Typecheck (100 pkg updated): 80.69 seconds, max memory 664.58 MB 5Typecheck (1 nested leaf pkg updated): 26.66 seconds, max memory 407.54 MB 6Typecheck (2 nested leaf pkg updated): 31.17 seconds, max memory 889.86 MB 7Typecheck (1 nested root pkg updated): 26.67 seconds, max memory 393.78 MB 8

These performance benefits will be more noticeable for larger repositories, but even small code bases will see some benefits.

Local TypeScript Path Aliases

When using project references, you can not define path aliases in the root tsconfig.base.json file because TypeScript does not merge the path aliases when doing the typecheck calculation, but you can set path aliases in an individual project's tsconfig.app.json file. For instance, you could define paths like this in an application's tsconfig file.

/apps/my-remix-app/tsconfig.app.json
1{ 2 "compilerOptions": { 3 "paths": { 4 "#app/*": ["./app/*"], 5 "#tests/*": ["./tests/*"], 6 "@/icon-name": [ 7 "./app/components/ui/icons/name.d.ts", 8 "./types/icon-name.d.ts" 9 ] 10 } 11 } 12} 13

Project Linking with TypeScript Path Aliases

Incompatible with TypeScript Project References

You can not use TypeScript project references with this style of project linking.

Linking projects with TypeScript path aliases is configured entirely in the tsconfig files. You can still use package manager workspaces to enable you to define separate third-party dependencies for individual projects, but the local project linking is done by TypeScript instead of the package manager.

The paths for each library are defined in the root tsconfig.base.json and each project's tsconfig.json should extend that file. Note that application projects do not need to have a path defined because no projects will import code from a top-level application.

/tsconfig.base.json
1{ 2 "compilerOptions": { 3 // common compiler option defaults for all projects 4 // ... 5 // These compiler options must be false or undefined 6 "composite": false, 7 "declaration": false, 8 "paths": { 9 // These paths are automatically added by Nx library generators 10 "@myorg/shared-ui": ["packages/shared-ui/src/index.ts"] 11 // ... 12 } 13 } 14} 15