35.7 Publishing TypeScript Packages from a Monorepo

Right, so you’ve built this beautiful monorepo, a symphony of interconnected packages, and now you want to share one of those masterpieces with the world. Publishing a TypeScript package isn’t just about running npm publish and hoping for the best. If you do that, you’ll end up shipping your src directory, your tsconfig.json, and probably your half-eaten lunch, which is not what consumers of your package signed up for. They want clean, runnable JavaScript and type definitions. Let’s get you from a messy workspace to a pristine published package.

35.6 Shared tsconfig Packages: Extending a Base Config

Right, so you’ve decided to organize your monorepo like a sane person. Good for you. But now you’re staring down ten different packages/ directories, each with its own tsconfig.json, and you’re about to copy-paste the same 20 lines of configuration for the ninth time. Stop. We’re not animals. We’re going to create a single, shared tsconfig package that everything else can extend. It’s the closest thing to a “set it and forget it” miracle you’ll get in this line of work.

35.5 Nx: Integrated TypeScript Monorepo Tooling

Right, so you’ve decided to manage the beautiful chaos of a monorepo. Good for you. It’s the only sane way to build a system out of interconnected parts without losing your mind to ../../../../ hell. But TypeScript, brilliant as it is, wasn’t originally designed with this multi-project, cross-reference madness in mind. You can cobble it together yourself with a mountain of tsconfig.json files and a prayer, but you’ll spend more time nursing the build system than writing code. This is where Nx comes in—not just as a task runner, but as a full-fledged build system that understands your project graph and, crucially, understands TypeScript.

35.4 Turborepo: Caching TypeScript Builds

Alright, let’s talk about the part of Turborepo that will save you from throwing your laptop out the window: caching. If you’re not using its caching superpowers for your TypeScript builds, you’re essentially paying for a sports car and never taking it out of first gear. It’s the difference between waiting for a full tsc --build every single time and, well, not doing that. The core idea is gloriously simple. Turborepo runs a function (a task in its parlance) and calculates a hash based on the inputs—your source files, tsconfig.json, environment variables, and any other dependency you specify. If it’s seen that hash before, it skips the arduous work of running the task and just plops the previous output (the compiled JS, .d.ts files, etc.) back into place from its cache. It’s like a time machine for your dist directory.

35.3 npm, yarn, and pnpm Workspaces with TypeScript

Right, let’s talk about workspaces. This is where the monorepo concept stops being a cute idea and starts being a real, tangible thing that saves you from an aneurysm. Without workspaces, a monorepo is just a directory full of separate projects that hate each other. Workspaces are the therapy that gets them to cooperate. They do two magical things: they hoist dependencies to avoid 47 copies of lodash, and they create symlinks so your packages can reference each other before you publish them to npm.

35.2 TypeScript Project References in a Monorepo

Right, let’s talk about Project References. This is TypeScript’s official, built-in answer to the question, “How do I make my monorepo not a complete nightmare to build?” It’s the feature that lets one TypeScript project (tsconfig.json) intelligently depend on another. It’s the difference between manually orchestrating a tsc --build in every package in the right order and just running one command from the root that figures it all out for you. It’s genuinely good stuff, but like most things in TypeScript, it has a few sharp edges you need to sand down.

35.1 Monorepo Challenges: Shared Types and Cross-Package Imports

Alright, let’s get into the real meat of the monorepo: sharing code without creating a circular dependency hellscape. You’ve set up your packages, they’re all in one repo, and now you want @your-app/ui to use a type from @your-app/utils. Seems simple, right? Welcome to the fun part. The first trap everyone falls into is thinking they can just import directly across packages using relative paths. Don’t. This is the monorepo equivalent of trying to dig a tunnel with a spoon. Your ui package should have no idea where your utils package lives on the file system. They are separate entities, bound by the laws of your package manager (npm, Yarn, pnpm). You must treat them as such. This means using the published package name in your imports, even though the code is sitting right next to you.

— joke —

...