DEV Community

Cover image for Using TypeScript Project References to share common code
James Wallis
James Wallis

Posted on • Updated on • Originally published at wallis.dev

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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" }
    ]
}
Enter fullscreen mode Exit fullscreen mode

 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 .
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

Note: if your compiler has problems with the IData file, you can add type after the import. See: https://dev.to/uppajung/comment/1f6bc

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!

Top comments (6)

Collapse
 
uppajung profile image
Stuart Schechter

Adding type after import can prevent the compiler from thinking that it needs to include an IData.js file or modules that try to grab IData.js.

In other words, I'd suggest changing the last code line to

import type IData from '../common/interfaces/IData'
Enter fullscreen mode Exit fullscreen mode

That would have saved me a few hours on my electron project.

Collapse
 
jameswallis profile image
James Wallis

Interesting! Thanks for adding this. I'll add a link to your comment in the post.

Collapse
 
klaster1 profile image
Ilya Borisov

The project I work uses the same frontend/backend/common style setup for shared types, but none of "compilerOptions" set "composite", "declarationMap" and "declaration". Are these options really required? What's the difference?

Collapse
 
jameswallis profile image
James Wallis

Hi Ilya, thanks for commenting.

I added composite, declarationMap and declaration as the docs say they are required or recommended.

From the docs:
composite (required): Referenced projects must have the new composite setting enabled. This setting is needed to ensure TypeScript can quickly determine where to find the outputs of the referenced project.

declaration is required when you use composite

declarationMap (recommended): We’ve also added support for declaration source maps. If you enable --declarationMap, you’ll be able to use editor features like “Go to Definition” and Rename to transparently navigate and edit code across project boundaries in supported editors.

Relevant doc sections: typescriptlang.org/docs/handbook/p...

I don't know what would happen if I removed these settings, however.

Collapse
 
teocns profile image
Info Comment hidden by post author - thread only accessible via permalink
teocns

How do you solve this error? Have followed the guide properly but when calling a type from outside the SRC project I get this erorr:

./src/Components/ActionsList.tsx
Module not found: You attempted to import ../../../common/Types/Action which falls outside of the project src/ directory. Relative imports outside of src/ are not supported.

Collapse
 
t7yang profile image
t7yang

I have wrote a similar topic article
dev.to/t7yang/typescript-yarn-work...

Some comments have been hidden by the post's author - find out more