37.7 Testing Your Published Types with @arethetypeswrong/cli

Right, so you’ve written your beautiful library, your types are a work of art, and you’ve run tsc and tsd and everything passes. You’re feeling pretty good, right? Don’t. You’ve only tested your types in situ, from the source. The real world is a much, much darker place. Your meticulously crafted .d.ts files get bundled, transformed, and ultimately consumed by a user’s project in ways that can make a complete mockery of your original intentions. This is where @arethetypeswrong/cli (or attw for short) comes in—it’s the final, brutal honesty your library needs before it faces the public.

37.6 Versioning Type Definitions Alongside Library Versions

Now, let’s talk about something that seems like it should be simple but is, in fact, a delightful little minefield: keeping your type definitions in sync with your actual library code. You’ve just shipped a fantastic new feature in my-awesome-lib@2.1.0. You’re feeling great. Then you get the issue: “Types are broken!!1!” You forgot to update the index.d.ts file. We’ve all been there. It’s the equivalent of putting on a sharp suit and then forgetting to wear pants.

37.5 The exports Field in package.json and TypeScript Resolution

Right, let’s talk about the exports field. This is where we graduate from “just publishing some files” to actually building a proper, thoughtful library. If you’re still using the old "main" and "types" fields, I’m not mad, just disappointed. It’s like using a flip phone in 2024—it works, but you’re missing out on a world of security, control, and sanity. The exports field in your package.json is your library’s bouncer. It explicitly tells the outside world (and TypeScript) which entry points are public and how to find them. Everything else? It’s off-limits. This is called “package encapsulation,” and it’s the single best way to avoid the dreaded “hey, why can my users import this internal file I never meant to expose?” problem. We’ve all been there. It’s not fun.

37.4 Supporting Multiple Module Formats: ESM and CJS

Right, so you’ve decided to be a proper library author. Congratulations, and my condolences. You’re about to enter the special hell of making your beautiful TypeScript code play nicely with the entire JavaScript ecosystem’s messy, decades-spanning module system. The goal is simple: someone using old-school require() in a CommonJS (CJS) app should be able to use your library, and someone using shiny import in an ESM app should be able to use it too, all without them ever knowing about the duct tape and baling wire you used behind the scenes.

37.3 Avoiding Declaration File Pitfalls: Overly Narrow Types

Right, let’s talk about one of the most common ways we, as library authors, accidentally create a hostile environment for our users: overly narrow types. You’ve poured your soul into crafting a beautiful, robust API, and then you ruin it by telling the user, “No, you can’t do it that way,” when their way is perfectly valid. It’s the library equivalent of being a pedantic dinner guest who corrects everyone’s grammar. Don’t be that person.

37.2 Generating and Bundling Declaration Files

Right, so you’ve built your library. It’s a masterpiece of type-safe engineering. But if you just ship the raw .ts files or, heaven forbid, the compiled .js without a map, you’re leaving your users in the dark. They get a any-shaped box and have to guess what’s inside. That’s not very friendly. The solution is the Declaration File (.d.ts), and getting it right is what separates the pros from the amateurs.

37.1 Designing a Public API with TypeScript

Alright, let’s talk about designing a public API. This is where you move from writing code for yourself to writing code for everyone else. It’s the contract you sign with your users, promising not to be a jerk and break their entire codebase with a surprise update at 2 AM. TypeScript is our not-so-secret weapon here, letting us write that contract in iron-clad, type-safe ink. But with great power comes great responsibility to not design something truly idiotic.

19.8 Triple-Slash Directives: /// <reference types="..." />

Right, let’s talk about triple-slash directives. No, they’re not a secret handshake or a way to comment out your entire life. They’re a pre-ES6, pre-module-world holdover that the TypeScript team keeps around because, well, sometimes you need an old key for a weird, specific lock. They’re essentially single-line XML comments (/// ) that live at the very top of your file and give instructions to the compiler. The one you’ll actually use in modern projects is almost exclusively <reference types="..." />.

19.7 Generating Declaration Files with tsc --declaration

Right, so you’ve written some beautiful TypeScript. It’s clean, it’s typed, it’s a work of art. But now you want to share your magnificent library with the poor souls still stuck in the jungles of plain JavaScript. You can’t just hand them the raw .js files; that would be like giving someone an IKEA flat-pack without the instruction manual. They’d have no idea how to use your functions without getting a splinter from a rogue any. This is where tsc --declaration comes in. It’s your compiler’s way of generating that instruction manual: a .d.ts declaration file.

19.6 DefinitelyTyped and @types Packages

Right, so you’ve written some TypeScript, you’re feeling smug about your type safety, and then you try to import lodash or react and your editor lights up like a Christmas tree with red squiggles. The library is pure JavaScript. Your brilliant type system has no idea what _.chunk() is supposed to do. This is where the magic—and the occasional dumpster fire—of DefinitelyTyped and @types packages comes in. Think of DefinitelyTyped as the world’s largest, most chaotic, and miraculously effective group project. It’s a massive GitHub repository where volunteers write and maintain declaration files (.d.ts) for libraries that don’t ship with their own types. When you run npm install --save-dev @types/lodash, you’re not installing code from Lodash; you’re installing a package automatically published from the lodash folder within the DefinitelyTyped repository. It’s a separate entity giving your type checker the blueprint it needs to understand that library’s shape.

19.5 Writing .d.ts Files for Untyped JavaScript Libraries

Alright, let’s roll up our sleeves and do some community service. You’ve found a brilliant JavaScript library that does exactly what you need. There’s just one problem: it’s from the era before TypeScript was cool, and it has no types. Your code is now a screaming parade of any types, and your autocomplete has given up and gone home. This is where you step in and write a Declaration File (.d.ts) to bring this library into the light.

19.4 Global Augmentation and Module Augmentation

Right, so you’ve met your first .d.ts file and you’re feeling pretty good. You can describe the shape of a library or your own code. But then you stumble into a codebase and see something like this: declare global { interface Window { myCrazyCustomProperty: string; } } Your first thought is, “Wait, declare global? What is this, some kind of incantation?” And your second thought is, “Why would I ever need this?” Buckle up, because we’re about to answer both. This is where we move from describing types to augmenting them—officially, and globally.

19.3 Ambient Modules: declare module

Right, so you’ve met declare module. This is how we tell TypeScript, “Hey, trust me, this thing exists over there, in some other file or library, and here’s what its shape looks like.” You’re essentially drawing a map for the compiler to a treasure that you’ve already buried elsewhere. It’s the backbone of describing non-TypeScript libraries, but it’s also got some sharp edges if you’re not careful. The most common, and frankly, the most sane use of declare module is for describing those classic JavaScript libraries that were written before anyone thought type safety was cooler than a flip phone. You use it to create an ambient module declaration.

19.2 Ambient Declarations: declare var, declare function, declare class

Right, so you’ve decided to step into the world of TypeScript’s ambient declarations. Good for you. This is where we stop just using types and start telling TypeScript about types that exist elsewhere. Think of it as being a translator for a system that doesn’t speak TypeScript natively—like a glob of vanilla JavaScript, a library loaded from a <script> tag, or some magic your build process injects. We use the declare keyword to shout into the void, “Hey TypeScript compiler! Trust me on this. This thing exists, and this is its shape. Now stop complaining about it.” It’s a promise, and if you break that promise at runtime, well, that’s on you. The compiler will have already packed up and gone home.

19.1 What Declaration Files Are and Why They Exist

Right, let’s talk about the digital equivalent of a restaurant menu: the TypeScript Declaration File, or .d.ts. You’ve written some beautiful, type-safe TypeScript. Then you npm install a library, and… nothing. Your editor lights up like a Christmas tree with Cannot find module 'cool-library' or its corresponding type declarations. What gives? The library is written in plain JavaScript. It exists, your Node runtime can require it just fine, but the TypeScript compiler has absolutely no idea what’s inside that box. Is it a beautiful porcelain vase? A pile of angry bees? It needs a description. A declaration file is that description. It’s a file full of only type information—no actual logic, no console.logs, just a meticulously typed map of what the JavaScript code looks like. It tells TypeScript, “Hey, inside cool-library, there’s a function called configureBees that takes an AngryBeeOptions object and returns a Promise<Honey>.” TypeScript breathes a sigh of relief and gets back to work. You get autocomplete, IntelliSense, and compile-time safety, all without the library author having to rewrite their entire project in TypeScript. It’s a genius compromise.

— joke —

...