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.

26.7 skipLibCheck and Its Trade-offs

Alright, let’s talk about skipLibCheck. This is one of those compiler options that sounds like a free performance boost, and honestly, it mostly is. But like most things that seem too good to be true, it comes with a small, gnarly catch. I’m going to explain what it is, why you should almost always turn it on, and what that catch actually means for you. In a nutshell, skipLibCheck: true tells the TypeScript compiler, “Hey, don’t bother doing a full type check on the .d.ts files (declaration files) in my node_modules folder.” Think of it as a bouncer at an exclusive club. Instead of meticulously checking the ID of every single person in the line (your dependencies’ dependencies’ dependencies), it just lets everyone in who’s already on the list, trusting that the list is probably correct. This saves a massive amount of work.

26.6 SWC and esbuild: Transpile-Only Builds for Speed

Right, let’s talk about speed. You’ve felt it, that agonizing lag between hitting save and your TypeScript project finishing its compile cycle. It starts as a minor annoyance and slowly grows into a soul-crushing time sink. The culprit? tsc, the official TypeScript compiler, is doing a lot of heavy lifting for you: type checking, transpiling to your target JavaScript, handling all those fancy tsconfig.json options. It’s brilliant, but it’s a scholar, not a sprinter.

26.5 Avoiding Expensive Type Patterns: Deep Recursive Types

Let’s be honest: you’re not thinking about TypeScript’s type system performance until your IDE starts to stutter and your tsc --watch feels like it’s running on a potato. That’s when you meet the deep recursive type. It’s the type-level equivalent of a Rube Goldberg machine—impressively clever, but you wouldn’t want to use it to make your morning coffee. The core of the problem is simple: some types are just expensive to compute. The TypeScript compiler is brilliantly fast, but it’s not magic. When you create a type that forces it to perform a deep, recursive calculation across a massive structure, you’re asking it to solve a complex puzzle. Every. Single. Time. You. Save. A. File.

26.4 Type-Only Imports and Reducing Declaration Emit Work

Right, let’s talk about one of the single biggest “aha!” moments for speeding up your TypeScript compilation: import type. This isn’t just a fancy syntax; it’s a cheat code that tells the TypeScript compiler, “Hey, relax, we’re not actually going to run this. I just need to know what it looks like.” Think of your average import statement as ordering a full, assembled piece of IKEA furniture. The delivery truck (your bundler) shows up, you get the massive box (the module’s code), and you have to haul it inside and put it together (include it in your JavaScript output). Now, imagine if you could just call the company, describe the furniture, and have them fax you the assembly instructions (the type definitions) without the physical box ever showing up at your door. That’s what import type does. It only brings over the type information and leaves absolutely no trace in your final JavaScript. This is a huge win because it reduces the amount of code the compiler and your bundler have to process.

26.3 Project References for Large Monorepos

Right, so your monorepo has gotten big. The node_modules directory has its own gravitational pull, running tsc feels like you’re asking your laptop to calculate the meaning of life, and you’re pretty sure you just saw the progress bar actually get slower. Welcome. This is where TypeScript’s Project References come in, and they are about to become your new best friend. They’re not magic, but they’re the closest thing we have to a free lunch in the TypeScript world.

26.2 Incremental Compilation: tsbuildinfo Files

Right, let’s talk about one of the few things TypeScript got genuinely, unambiguously right for performance: incremental compilation. If you’ve ever sat there, drumming your fingers, waiting for a full project rebuild after changing a single comma, this is the feature that stops that particular brand of madness. At its core, incremental compilation is a simple concept: instead of rebuilding the entire world from scratch every time you run tsc, the compiler saves a little file of its own homework—a .tsbuildinfo file—that tells it exactly what work it doesn’t need to redo. It’s the compiler’s version of “I’ll just leave these parts out and only fix the bit I messed up.”

26.1 Diagnosing Slow TypeScript Builds: --diagnostics and --extendedDiagnostics

Right, so your builds are slow. You’ve felt that creeping dread as you hit tsc and go make a coffee, only to return and find it’s still chugging away. Welcome to the club. The first rule of TypeScript Performance Club is: you don’t just guess what’s wrong. You use the tools designed to tell you. That’s where --diagnostics and its more verbose cousin, --extendedDiagnostics come in. Think of them as the MRI machine for your ailing compilation process.

— joke —

...