In my previous life, I wrote in C++ and later C#, and used both to share code by compiling it to a “library file,” which I and my team could then copy to other projects. Sharing code libraries like this is important so that we aren’t rewriting or duplicating common code again and again for new projects.
It seems like it should be as simple to do this in TypeScript. We just compile to JavaScript (which we can think of as our new version of the “library file”) and then copy the JavaScript code to other projects.
In practice, though, it’s not so simple. By definition, complex applications — like the modern ones we’re building in TypeScript these days — are composed of multiple interacting parts.
Have you ever been coding in TypeScript and seen an error message like this?
File 'X’ is not under 'rootDir' 'Y'. 'rootDir' is expected to contain all source files. ts(6059)
This means you’re trying to import code from outside your current project. Most likely, you’re just trying to share code from one project to another!
But, how do we actually share code libraries between projects?
One way is to create separate code libraries that factor our application by functionality, allowing us to reuse our most commonly used and useful code between projects. See a representation of this in Figure 1.
In fact, there has been no elegant way to do this until TypeScript v3.0 — but we’ll get to that soon.
This blog post demonstrates the most recent results of my search to find a similarly lightweight way to share code libraries in TypeScript. In this post, we’ll fix this issue before moving onto more advanced methods of sharing TypeScript code, such as sharing between microservices and Docker images or sharing between the backend and the frontend.
We can get a lot of value from this, even within a single application. When the nature of the application’s structure is to be separated into components (e.g. microservices, backend/frontend, micro frontends, etc.), we can easily see the need to share common functions and data structures within a single application.
If you want to DRY your code across application components, this is what you must know.
Before we tackle the example code, let’s briefly review the standard ways of sharing code in the JavaScript community.
The most common method is to publish our code to the npm registry, so it can be installed as a dependency in any other project where we’d like to use it.
An alternative is to publish our code to a Git repository (it doesn’t have to be GitHub, but usually it is) and then use npm to install it directly from there.
Keep in mind that either method, publishing to the npm registry or publishing to GitHub, can be done either publicly or privately. Publishing publicly is the default, of course, but you can also publish privately when working on a codebase that is not open source.
These most common JavaScript publication approaches are relevant because they are also the most common way to publish TypeScript code. (If you need a quick refresher on this, have a look at this publishing guide.)
It’s rare that we’d ever normally publish TypeScript code directly instead of compiling it to JavaScript and publishing that. For many projects, though, publishing a shared code library seems like overkill.
For one, it’s a massive impediment to fast development, and two, it seems like a waste of effort to publish a library that I’ll only use for a handful of microservices, or when I want to reuse a data model on either side of a REST API. Really, it’s not usually worth publishing unless you want to share your data model with the world.
During routine coding, I make frequent and iterative changes across the codebase. (When you do have to work this way for npm packages, make sure you’re using npm-link.) Some of these changes will turn out to be unnecessary or bad (e.g. they break something), but we need the freedom to experiment with our code and then back out of it when it’s not working out. It’s fundamental that we can experiment and test our work without needing to commit and publish it first.
I usually ask myself the following questions before publishing my code libraries on npm:
Still, there are some other cases where publishing to npm can be a drag on efficient development — and that’s where we need to find alternative methods of sharing our code.
So, above, we established the following guidelines for developing an alternative method of sharing code:
With that in mind, let’s move on to the main event: looking at alternative means of sharing TypeScript code libraries between our projects.
In this blog post, we’ll walk through code from several examples.
The first example is a basic but necessary starting point. You need to install Node.js to run it.
The second example is more advanced and shows you how to share a code library into a Docker-based microservice. You need to install Docker to run it.
The third and final example shows how to share code between a Docker-based microservice and a frontend built with React, TypeScript, and Parcel. You need both Docker and Node.js installed to fully build and run this example, but if you want to build the frontend only, you just need Node.js.
The code is available on my GitHub, and you can either clone the code for yourself if you’d like to follow along:
git clone [email protected]:ashleydavis/sharing-typescript-code-libraries.git
Or download the zip file and unpack it to your local computer.
The methods I present below allow for a flexible project structure. You can use these techniques with a mono-repo, or a meta-repo when using the meta-tool. You don’t have to use any repo if, for some weird reason, you don’t use version control — instead, you can use these techniques with an ad hoc directory structure on your local computer.
These examples do follow a particular layout, but that’s not really important. You can arrange the files and folders to suit your own taste and needs.
Finally, the methods presented here are also scalable. You can add more shared code libraries to each example and you can add more main projects (e.g. a project for each microservice).
Please feel free to skip this section if you already understand how to use TypeScript project references.
TypeScript project references are actually quite new! They have only been available since TypeScript v3.0, which was released in 2018. According to the documentation, project references allow you to structure your TypeScript programs into smaller pieces, which helps improve build times, enforce logical separation between components, and organize your code in new and better ways.
Let’s start with the most basic example: a Node.js “Hello, World!” program that shows how to include a shared TypeScript code library to a Node.js project. This is fairly simple, but it is a necessary building block for the more advanced examples.
This example uses only a single shared library, but you can add more by adding references
to your tsconfig.json
file. Each of the referenced projects must be a valid TypeScript project and have its own tsconfig.json
file.
Figure 2, below, explains the structure of the example project. Feel free to explore it for yourself in GitHub or on your local computer if you clone the project.
The code for the shared library is presented Listing 1, below. It prints “Hello, World!” to the console.
Listing 1: Code from the shared library
export function showMessage(): void { console.log("Hello world!n"); }
The code for the main project imports the showMessage
function from the shared library and calls it, as shown below in Listing 2.
Listing 2: The main project uses code from the shared library
import { showMessage } from "../../libs/my-library"; showMessage();
Like I said earlier, this example couldn’t be much simpler.
The interesting part is how project references are configured for the main project in its tsconfig.json
file. An extract is shown below in Listing 3.
Listing 3: Extract from the main project’s tsconfig.json
"references": [ { "path": "../libs/my-library" } ]
In Listing 3, we define an array of references
to other TypeScript projects. This connects our main project to its shared libraries.
So how do we build the Node.js example project?
First, open a terminal window and navigate to the main repo’s directory, then into the Node.js example:
cd sharing-typescript-code-libraries/nodejs-example
Now, navigate down to the main project:
cd my-project
We can build the project now.
For an ordinary TypeScript project, we would invoke the TypeScript compiler, like this:
npx tsc
But, now that we are using TypeScript project references, we must add the --build
argument to the end:
npx tsc --build
The --build
argument causes the TypeScript compiler to read the references
field from tsconfig.json
. It then builds each referenced project before building the main project.
That’s it. At the most basic level, there’s nothing more to it.
This is a huge time saver! It means we don’t have to separately invoke npx
tsc
for each shared code library and, importantly, it means we’ll never forget to build a code library after changing its code — which will save us a lot of time spent wondering why our code change isn’t coming through to the main project.
As part of my personal convention, I wrap this up in an npm script called build
. Visit the package.json
file if you want to see what it looks like. Doing this means that I can invoke npm run build
for any project, and it will automatically translate to npx tsc --build
for TypeScript projects with shared libraries. I feel this is a nice touch because it means I don’t have to remember to add --build
each time.
The good thing about this method for sharing is that it’s highly extensible. We can scale this up to many more shared code libraries and all we need to do is compile the main project.
Let’s consider a more advanced example. Imagine we are creating a microservices application where each microservice is deployed from a Docker image. We have compiled code that we’d like to share between microservices and we need to bake it into each Docker image.
Though this will be more advanced, the example code here is essentially the same as in the previous example, except this time we’ll compile our main project and shared library within the Docker build process. We’ll then bundle and copy the compiled code into our production Docker image.
Figure 3 explains the structure of this project.
The important part of this example project is the Dockerfile we use to build the Docker image.
Figure 4 is an annotated version of the Dockerfile highlighting the most important parts.
Notice how we copy the entire root project here. This copies the code for our shared libraries and the main project into the build stage for our Docker image.
Next, we invoke npm run build
to compile TypeScript to JavaScript and bundle the results. Then, we use the recursive-install tool to install production-only dependencies for the shared library and the main project.
Ultimately, to generate a production Docker image we must copy the compiled code bundle from the build stage into the final image. The final image should omit the TypeScript source code and dev dependencies, which simply aren’t necessary in production. Only the compiled JavaScript code and production dependencies are copied across.
So, we can say that the production image is lean and not bloated with the unnecessary debris of development, debugging, and testing.
One question remains. How exactly do we bundle the compiled JavaScript code?
You didn’t miss that. It is actually hidden within the npm run build
. You can see how this works in Listing 4, which is an extract from the package.json
file for the main project.
Listing 4: Extract from package.json
"scripts": { "build": "tsc --build && ts-project-bundle --out=build", },
Our problem is that the compiled JavaScript code is embedded in each of the separate TypeScript projects. We must separate the compiled code, leaving behind the original TypeScript source code that we don’t need in production.
We have a few options for a solution. We could easily copy the code into our production Docker image by adding a bunch of copy commands to our Dockerfile, but then we’d need new copy commands for any new libraries we added, which is not very scalable. Plus, it’s better if we can have something that will instead copy what we have now and anything we might add in the future.
This is where ts-project-bundle
comes into play, as you can see above in Listing 4. This is a small simple command-line tool that I created (and shared via npm, of course, because I want everyone to use it).
It reads the tsconfig.json
files for the main project first, then for each referenced project. It then copies the compiled code to your output directory of choice. This is how the compiled JavaScript code is placed in the build
directory and is ready to be copied into the final production Docker image.
Incredibly, this kind of code extraction and bundling isn’t included in the TypeScript compiler — and I hope they add this in the future and make ts-project-bundle redundant.
Now, we want to build the Docker image.
Navigate to the directory of the main project, then invoke the Docker build command like this:
docker build .. -f ./Dockerfile -t hello-world
Note how we are using the parent directory as the build context. This is because the Docker build stage needs access to the parent directory, which includes the code for both the main project and the shared libraries.
The Dockerfile is in the same directory as the main project to keep it relevant and connected to that microservice. Other microservices should also have their own Dockerfiles, and you may want to share a templated Dockerfile — but that’s a different blog post.
Because the Dockerfile is in a different directory to the build context, we have to specify it manually and that’s why we use the -f
argument above.
After building the Docker image, you can run a container like this:
docker run hello-world
This method of sharing code is scalable. Once again, the example here is relatively simple, but we can easily add more code libraries and microservices to it. To recap:
ts-project-bundle
to bundle the code for a microservice, including any and all libraries that it depends onrecursive-install
to install npm dependencies for each microservice and all of its shared librariesIf you’re wondering what the bundled code looks like, please see figure 5 below. You may also like to build the code for yourself and inspect it. You won’t even need Docker installed to do that — just navigate to the main project, invoke npm run build
, and poke around in the generated build
subdirectory to see the compiled and bundled code.
This example also has a Docker microservice, but it works the same way as in the previous example, so I won’t explain that again.
We’ll now focus on sharing the code library to the frontend. The example UI is created with TypeScript and React, and is bundled with Parcel.
Figure 6 explains the structure of this project.
This example has the directories backend
, frontend
, and libs
. Again, this project layout is extensible: you can add more libraries to the libs
directory and more microservices to the backend
directory.
The structure is also flexible: you can put it all in a mono-repo or you can fully separate it out as a meta-repo with individual code repositories for each microservice, each library, and the frontend.
Listing 5 shows the simple HTML code for our example frontend.
Listing 5: Simple HTML file for the frontend
<!DOCTYPE html> <html lang="en"> <head> <title>A simple React frontend using Parcel</title> </head> <body> <div id="root"></div> <script src="./src/index.tsx"></script> </body> </html>
This example uses the React framework, and in Listing 5 you can see the root
element where our React UI will be rendered. The subsequent script
tag imports the TypeScript code file index.tsx
. This is the main code file for our React UI. It is also the only TypeScript code file in the main project because this is a simple example.
Listing 6 shows index.tsx
using React to render the UI for the frontend. Notice how we are importing the function from our shared library and using it to render the “Hello, World!” message in the frontend.
Listing 6: React code for the frontend
import React from "react"; import ReactDOM from "react-dom"; import { showMessage } from "../../libs/my-library"; class App extends React.Component { render() { return <div>{showMessage()}</div>; } } ReactDOM.render(<App />, document.getElementById("root"));
To link to the shared library, we again use TypeScript project references. You can see this for yourself in the frontend’s tsconfig.json
file, but it isn’t different from what you’ve seen in the earlier examples.
To make this frontend usable in a web browser, we must now compile it to a static web page. Parcel makes this easy. It’s a zero-configuration bundler that automatically understands TypeScript. It understands most common asset types out of the box, and for everything else, there’s a plugin.
I’m currently using Parcel v1 but will convert to v2 when it’s more mature. If you want to learn more, the Parcel docs have a great section on using TypeScript and React.
To compile our frontend, we’ll invoke parcel build
. I’ll remind you of my personal convention, where I wrap this up in npm run build
— remember that this is helpful because, whatever type of project I’m in, I only have to remember this one command instead of trying to remember the vagaries of different command line tools.
You can see what this looks like in Listing 7 below, which is a simplified extract from the frontend’s package.json
.
Listing 7: Extract from package.json showing npm scripts
"scripts": { "build": "tsc --build && parcel build index.html --out-dir=out", },
Listing 7 first invokes the TypeScript compiler using the --build
argument to build the frontend project and all the shared libraries. Then we invoke parcel build
and point it at index.html
, which in turn points at index.tsx
and specifies the output directory. This compiles our TypeScript code to JavaScript and bundles it into a single JavaScript file that is included in the compiled HTML file.
You should try running this and confirm for yourself that the generated static webpage indeed contains the code from our shared library. Navigate to the frontend
directory and either run this:
npx tsc parcel build index.html --out-dir=out
Or, more simply, run this:
npm run build
Now, navigate to the out
subdirectory to see the compiled HTML and JavaScript files. Search for “Hello, World!” and you’ll find the code from the shared library that has been bundled into the static webpage.
That’s all there is to it. I told you this example was less complicated than the last!
Now, you might say, this is great for Parcel, but what about bundler X? My first choice for this was actually not to use Parcel — I tried to build this example using Create React App, but unfortunately, React doesn’t yet support project references.
In my own development, I’m moving away from webpack because Parcel is much simpler, so I haven’t yet tried this with webpack. I’d be surprised if it didn’t work because under the hood, webpack will use the TypeScript compiler — or possibly Babel — so project references should work.
If you do try to implement this with webpack or any other bundler, please let me know how it works out for you!
In this post, we’ve explored three different ways to share TypeScript code libraries. After briefly looking at the standard ways to share code (via the npm registry or GitHub), we explored alternative, lightweight approaches to sharing libraries between components in a larger application, like between microservices or between the backend and frontend.
The examples I have presented here are extensible. We can scale them up to include more code libraries, more microservices, etc., and this approach works well either with a mono-repo or a meta-repo. It even works well with just ad hoc folders on your computer — although, in that case, I’m not sure why you aren’t using version control, but hey.
The missing piece of the puzzle that I built for myself was ts-project-bundle, which you can find on npm and GitHub. I hope someday that ts-project-bundle will be a relic of history and that the TypeScript compiler itself will support an effective means of bundling or exporting compiled JavaScript code. Let’s hope they correct this because it seems like an obvious omission to me!
If you like what I’ve written here, please also watch my video on the topic. You can also follow me on Twitter.
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowEfficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
Design React Native UIs that look great on any device by using adaptive layouts, responsive scaling, and platform-specific tools.
Angular’s two-way data binding has evolved with signals, offering improved performance, simpler syntax, and better type inference.
Fix sticky positioning issues in CSS, from missing offsets to overflow conflicts in flex, grid, and container height constraints.
One Reply to "Make sharing TypeScript code and types quick and easy"
Interesting article. Compilers/transpilers/linters are awesome. Coming from C/C++ myself, it is easy to try and become a compiler instead of having one do that job for you.
DRY is an anti-pattern. But the modularity you mention is SOLID + KISS principle.
I highly recommend checking out deno and getting react to run under it. There is even a npm package that installs deno to node_modules so you can migrate away from node at your own pace. Deno is created by Ryan Dahl, the creator of node.