Module Augmentation & Publishing Types
In Declaration Files & DefinitelyTyped you learnt where library types come from and how to describe an untyped package. Real projects soon need the next step: adding to types that a library already has. Your login middleware puts a user on every Express request; your design system adds a festive colour to a UI kit's theme; your bundler lets you import .svg files. TypeScript knows none of this until you tell it.
This lesson covers module augmentation (adding to a library's types without editing its files), declarations for non-code imports, the "registry" pattern that modern libraries use, and how to publish your own package so its users get good types.
Augmenting a library's interface
Interfaces with the same name merge (see Interfaces vs Type Aliases). Module augmentation uses this across files: you reopen a module with declare module "name" and add members to its interfaces.
Suppose a UI kit package called kirana-ui exports a ThemeColors interface and a color(name) function that accepts keyof ThemeColors. Your shop wants an extra festive colour for Diwali:
// src/theme.d.ts
import "kirana-ui";
declare module "kirana-ui" {
interface ThemeColors {
festive: string;
}
}// src/index.ts
import { color } from "kirana-ui";
export const banner: string = color("festive"); // now allowed
export const bad: string = color("accent"); // still an errorsrc/index.ts(4,34): error TS2345: Argument of type '"accent"' is not assignable to parameter of type 'keyof ThemeColors'.
Three rules make augmentation work:
- The augmenting file must be a module (it has at least one
importorexport). Theimport "kirana-ui";line does that and makes sure the original module is loaded first. - The string in
declare module "kirana-ui"must match how you import the package. - You can only add to things the module already declares, such as extra members in an existing interface. You cannot replace a declaration or change a property's type.
Many libraries are designed for this and tell you in their docs which interface to augment: a theme interface in UI libraries, a matchers interface in test runners, a Register interface in TanStack Router.
Adding user to Express requests
The most common augmentation in Node.js work is a property that middleware adds to the request object. Without it, TypeScript rejects req.user:
src/app.ts(7,12): error TS2339: Property 'user' does not exist on type 'Request<{}, any, any, ParsedQs, Record<string, any>>'.The Express types (@types/express) declare their Request interface inside a global namespace called Express precisely so you can extend it. Put this in a .d.ts file inside your src folder:
// src/types/express.d.ts
import type { SessionUser } from "../models.js";
declare global {
namespace Express {
interface Request {
user?: SessionUser;
}
}
}Now every handler sees req.user as SessionUser | undefined. Making it optional is honest: requests that skip the login middleware have no user, so handlers must check if (!req.user) before using it. This is one of the few places where you still write the namespace keyword in modern code, because the library chose that shape.
Environment variables work the same way. In a Vite project, the src/vite-env.d.ts file can list your variables so import.meta.env.VITE_API_URL is typed:
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
readonly VITE_RAZORPAY_KEY_ID: string;
}Remember that this only describes what you expect. Validating the real values at startup is covered in Runtime Validation & Type-Safe APIs.
Declaring CSS, SVG and other imports
Bundlers let you import files that are not JavaScript. TypeScript 7 checks even side-effect imports by default (the noUncheckedSideEffectImports option), so a plain import "./styles.css" fails unless something declares it:
src/app.ts(2,8): error TS2882: Cannot find module or type declarations for side-effect import of './styles.css'.
A wildcard module declaration describes every file that matches a pattern:
// src/types/assets.d.ts
declare module "*.css";
declare module "*.svg" {
const url: string;
export default url;
}Now import "./styles.css" is accepted and import logoUrl from "./logo.svg" gives a string. In a Vite project you rarely write these yourself, because vite/client types already declare CSS, images and other assets. You will write them for custom loaders or older webpack setups.
The registry pattern
Some libraries leave an empty interface for you to fill in, then compute their types from it. This lets a library know about your app's routes, events or feature flags without you passing type arguments everywhere. Here is the idea in one runnable file; in real code the first part lives in the library and the second in your app:
// ---- library part ----
interface FlagRegistry {}
type FlagName = keyof FlagRegistry;
const flagValues = new Map<string, unknown>();
function setFlag<K extends FlagName>(name: K, value: FlagRegistry[K]): void {
flagValues.set(name, value);
}
function getFlag<K extends FlagName>(name: K): FlagRegistry[K] {
return flagValues.get(name) as FlagRegistry[K];
}
// ---- app part: fill in the registry ----
interface FlagRegistry {
upiAutopay: boolean;
maxCartItems: number;
festivalBanner: "diwali" | "eid" | "pongal" | "none";
}
setFlag("upiAutopay", true);
setFlag("maxCartItems", 25);
setFlag("festivalBanner", "diwali");
console.log(getFlag("maxCartItems") + 5);
console.log(getFlag("festivalBanner").toUpperCase());
// setFlag("maxCartItems", "25"); // error: string is not a number30 DIWALI
Because both FlagRegistry declarations merge, FlagName becomes "upiAutopay" | "maxCartItems" | "festivalBanner", and every call is checked against the right value type. With a published library, the app part would be written as declare module "the-library" { interface FlagRegistry { ... } }.
Publishing your own types
When you publish a TypeScript package (to npm or to your company's private registry), you ship compiled JavaScript plus declaration files (.d.ts) that describe it. Turn on these options:
{
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"declaration": true,
"declarationMap": true,
"isolatedDeclarations": true
}
}declaration writes a .d.ts next to each .js file. declarationMap lets "Go to Definition" in your users' editors jump to your .ts source instead of the .d.ts. Then point to the files from package.json, with the types condition listed first inside exports:
{
"name": "@sharma-school/fees",
"version": "1.0.0",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"files": ["dist"]
}isolatedDeclarations (TypeScript 5.5 and later) asks you to annotate everything you export, so that fast tools can write declaration files without running the full type checker. It reports exports whose types would have to be inferred:
export function fee(classNo: number) {
return classNo * 1000;
}
export const config = { city: "Pune", fees: [1, 2] };src/app.ts(2,10): error TS9013: Expression type can't be inferred with --isolatedDeclarations. src/app.ts(4,45): error TS9017: Only const arrays can be inferred with --isolatedDeclarations.
Adding : number to the function and a type to config fixes both. Explicit export types are also good documentation and keep your public API stable. Before publishing, many teams run the @arethetypeswrong/cli tool, which checks that the types resolve correctly for both Node.js and bundler users.
Common mistakes
- Forgetting to make the augmentation file a module. Without any
importorexport,declare module "x"in that file replaces the module's types instead of adding to them, and everything else from the package disappears. - Putting the
.d.tsfile outsideinclude. Iftsconfig.jsondoes not include the file, the augmentation silently does nothing. - Making middleware properties required.
user: SessionUserlies on routes without login. Useuser?:. - Listing
typesafterdefaultinexports. Conditions are matched in order, sotypesmust come first. - Using
declare module "*.css"for CSS Modules. Class-name imports need a declaration with an object type, or a tool that generates them.
Key points
- Module augmentation reopens a library module with
declare module "name"and merges new members into its interfaces. - The augmenting file must be a module, and you can only add members, not change existing ones.
- Express request properties are added through
declare global { namespace Express { interface Request {...} } }. - Wildcard declarations such as
declare module "*.svg"type non-code imports; TypeScript 7 checks side-effect imports by default. - To publish types, use
declaration,declarationMap, anexportsmap withtypesfirst, and considerisolatedDeclarations.
Exercise
Your school app uses a package sms-kit that exports interface Templates { otp: { code: string } } and function send<K extends keyof Templates>(to: string, name: K, data: Templates[K]): void. Write an augmentation file that adds a feeReminder template with { studentName: string; amount: number }, then write a correct call and one call TypeScript should reject.
Show solution
// src/types/sms-kit.d.ts
import "sms-kit";
declare module "sms-kit" {
interface Templates {
feeReminder: { studentName: string; amount: number };
}
}// src/reminders.ts
import { send } from "sms-kit";
send("9876500000", "feeReminder", { studentName: "Ishaan", amount: 4500 }); // ok
send("9876500000", "feeReminder", { studentName: "Ishaan" }); // error: amount is missingThe interface merges with the library's Templates, so keyof Templates now includes "feeReminder" and its data is checked like any other template.
Quick Quiz
1. What does module augmentation let you do?
Augmentation merges new declarations into existing ones; it cannot replace them.
2. Why does the augmenting file need an import or export?
Inside a module, declare module is an augmentation; inside a script it is a full declaration.
3. In TypeScript 7, why can import "./styles.css" be an error?
A wildcard declaration such as declare module "*.css" or the bundler's types fix it.
4. In a package's exports map, where should the types condition go?
Conditions are matched in order, so types must come before the JavaScript entry.
Learn TypeScript with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
