Using TypeScript Project References to share common code

Using TypeScript Project References to share common code

Ever wondered if you can share interfaces, types and functions between TypeScript projects?

I'm currently developing a project consisting of two separate TypeScript applications, one being a React.js dashboard and the other an Azure Function app written in Node.js. As part of the project, the dashboard calls an API in the Azure Function app. This got me thinking, as I'm in control of both the data source and the application that uses the data, is there a way that I can share certain interfaces between the two projects?

The answer is yes, since version 3 of TypeScript you can use Project References to share code between TypeScript projects. When using Project References in my project, however, I couldn't find any official examples on how to use them - hence this post!

While the implementation below is what has worked for me, if you have any improvements, let me know in the comments.

What are Project References?

Project references allow you to structure your TypeScript programs into smaller pieces. By doing this, you can greatly improve build times, enforce logical separation between components, and organize your code in new and better ways.

How to use

Take a project that consists of a frontend and a backend written in TypeScript. Both contain an interface called IData which is exactly the same. Currently, each time I make a change, I have to duplicate it in the other file (which is extremely annoying).

The directory of the project is:

myproject
- frontend
  - app.ts
  - interfaces
    - IData.ts
  - tsconfig.json
- backend
  - server.ts
  - interfaces
    - IData.ts
  - tsconfig.json

In order to use a single IData.ts file between both projects, we can use Project References.

Adding the common TypeScript project

We will start by creating a third TypeScript project called common, adding an empty tsconfig.json file and copying the IData.ts interface over. We can also remove it from the frontend and backend apps. So the directory structure will be:

myproject
- frontend
  - app.ts
  - tsconfig.json
- backend
  - server.ts
  - tsconfig.json
- common
  - interfaces
    - IData.ts
  - tsconfig.json

This isn't enough though. In the common app's tsconfig.json we need to add the following:

{
    "compilerOptions": {
        "target": "es5", // Or whatever you want
        "module": "es2015", // Or whatever you want
        "declaration": true,
        "declarationMap": true,
        "outDir": "./dist",
        "composite": true
    }
}

The key parts are:

  • declaration: Generates a declaration file that the frontend and backend apps can use to reference items in the common app.
  • composite: Ensures TypeScript can quickly determine where to find the outputs of the referenced project
  • declarationMap: Enables editor features like “Go to Definition” and Rename to transparently navigate and edit code across project boundaries in supported editors

Referencing the common project in frontend/backend

To reference the common IData interface in the frontend and backend apps we need to make a simple change to both of their tsconfig.json files. Add the references property to your existing tsconfig.json.

{
    "compilerOptions": {
        // The usual
    },
    "references": [
        { "path": "../common" }
    ]
}

Building the frontend/backend apps

Now that we've added the reference to the common app in order to access its interfaces we need to compile both the frontend and backend apps.

When doing so, ensure you use the --build option so that TypeScript automatically builds all referenced projects.

tsc --build .

Note: If you're using Next.js with TypeScript, I didn't need to do this. Both next dev and next build kept working just the same.

Importing the common interface into frontend/backend

This is easier than you might first think, just import IData using its relative path. TypeScript will do the magic when you compile it.

import IData from '../common/interfaces/IData'

Summary

In this post, I've demonstrated how to use TypeScript Project References to use a common project for shared interfaces, functions, types and more!

Feedback on my approach is appreciated! As I said above, I couldn't find an official example to guide me on how to use Project References so any feedback in the comments will help me improve this tutorial and my own TypeScript projects!

Thanks for reading!