In TypeScript, how do you add a property to an interface exported by a third-party package's type declarations — say adding `user` to a framework's `Request` interface — without forking or copying that package's types, and what does the compiler require of the file you write it in?
answer
- re-open the package's declaration
- interface merging does the work
- augmenting file must be a module
- script file declares instead of augments
- additive only, no new top-level names
basics
~10 sUse module augmentation: in a file that is already a module, write declare module 'the-package' and re-open the exported interface with your extra members. Interface merging adds them everywhere the package's types are used.
solid answer
~50 sWrite a **module augmentation**: `import 'the-package'; declare module 'the-package' { interface Request { user?: User } }`. Because interfaces merge, your members are added to the package's own `Request` everywhere it is used, with no fork. Three requirements decide whether it works. The file must itself be a module — with a top-level `import` or `export` — otherwise the same syntax *declares a new ambient module* that shadows the real package's types instead of extending them. The specifier must resolve exactly as it would from an import in that file, and the target must already have type declarations, since an untyped module cannot be augmented. And the file must be in the program, via `include`/`files` or by being imported. Augmentation may only add members to existing declarations; it cannot introduce new top-level names or change an existing member's type.
code
typescript · 10 lines// src/types/framework-augmentation.d.ts
import 'some-framework'; // makes this file a module
import type { User } from '../auth/user';
declare module 'some-framework' {
interface Request {
// optional: only set after the auth middleware has run
user?: User;
}
}go deeper
Know that you extend a library's types by re-opening its interface with declare module rather than editing node_modules, and that interfaces merge while type aliases do not.
Explain the module-versus-script requirement and what goes wrong when it is missed, that the specifier must resolve to a module that already has types, and that augmentation is additive only.
Diagnose a silently ignored augmentation — program membership, or naming a re-exporting entry point instead of the declaring module — and insist on optional typing for properties a middleware may never set.
Treat augmentations as a shared, unscoped contract: decide where they live, cap how many the codebase carries, and weigh a wrapper type or explicit context object against patching a dependency's interface for everyone.
## The problem augmentation solves You depend on a package whose types you cannot edit, and you need one more property on a type it exports — a `user` on a request object, a custom field on a config interface, an extra method on a class. Copying the package's declarations into your repo works once and then rots. Module augmentation lets you re-open one of its declarations from your own code. ## The shape of an augmentation ```ts // src/types/framework.d.ts import 'some-framework'; // makes this file a module import type { User } from '../auth'; declare module 'some-framework' { interface Request { user?: User; } } ``` Inside a module, `declare module 'x'` means *augment* the module `x`. The `interface Request` inside merges with the package's own `Request`, exactly as two interface declarations in one file would merge, and the added member is then visible at every site that uses that type — including inside the package's own signatures. ## Requirement one: the file must already be a module This is where most failures come from, and it is the same script-versus-module rule that governs global declarations. If the file has no top-level `import` or `export`, it is a script, and `declare module 'some-framework' { ... }` **declares a new ambient module** with that name rather than augmenting the real one. The compiler does not complain; it simply prefers your declaration, so the package appears to export nothing but `Request`, and every other import from it breaks. A bare `import 'some-framework';` at the top — which is also honest about the dependency — is the usual way to make the file a module. ## Requirement two: the specifier must resolve The string in `declare module` is resolved the same way an `import` from that file would resolve it. Two consequences: - A relative specifier is resolved relative to the augmenting file, so augmenting your own module from a different directory needs the correct relative path. - You must augment the module that actually **declares** the interface, which is not always the one you import. Packages frequently re-export types from an internal entry point, and the augmentation has to name the declaring module for the merge to land. If the target has no type declarations at all, the compiler rejects the augmentation: an untyped module cannot be augmented, because there is nothing to merge with. ## Requirement three: it must be in the program A `.d.ts` is never imported by source code, so it enters the compilation only through `files`/`include` in tsconfig. If you instead put the augmentation in a `.ts` file, that file must be imported by something for its augmentation to take effect. "My augmentation is ignored" is far more often a program-membership problem than a syntax problem. ## What augmentation may and may not do - It may **add** members to existing interfaces, classes (through interface merging) and namespaces. - It may **not introduce new top-level names** in the module — you cannot invent an export the package does not have. - It may **not change** an existing member's type. Re-declaring a property with a different type is an error, not an override; if you need a different shape, you need a wrapper type, not an augmentation. - It cannot augment a **default export** as such — merging works by exported name, so you must target the named declaration. ## The judgment part Augmentation is global to the program. Once `user?: User` exists on `Request`, it exists for every file, every middleware, every library that touches that type — including code paths where the middleware that sets it never ran. That is why the property is usually declared optional: the optionality is the only honest signal that it may be missing. And, as with every part of the type layer, nothing here is verified at runtime — the augmentation emits no JavaScript and does not make anyone assign `req.user`. When two packages augment the same interface with the same member name and incompatible types, you get a conflict you cannot easily resolve, since both declarations are outside your control. In a large codebase it is worth keeping augmentations few, centralised in one declarations file, and reviewed like any other shared contract.
- What happens if the file containing your `declare module 'some-framework'` block has no top-level import or export?It is a script, so the block declares a brand-new ambient module named `some-framework` instead of augmenting the real one. The compiler then treats your declaration as that package's types, so every other import from it fails to resolve its exports. It fails silently in the sense that the augmentation line itself reports no error. Adding `import 'some-framework';` fixes it.
- Can you use augmentation to change the type of a property the package already declares?No. Interface merging is additive: re-declaring an existing member with a different type is a conflict error, not an override, and identical re-declaration is merely redundant. If you need a different shape, build your own type — an `Omit` of theirs intersected with your replacement — and use it at your own boundaries rather than trying to rewrite the package's contract program-wide.
- Why is an added property like `user` almost always declared optional?Because augmentation is program-wide but the value's existence is not. The property is only populated after a specific middleware runs, yet the type applies to every request object in every file. Declaring it `user?: User` forces call sites to handle absence; declaring it required tells the checker something false and turns a missing middleware into a runtime `undefined` instead of a compile error.
- Your augmentation compiles but has no visible effect anywhere. Where do you look first?Program membership. A `.d.ts` is never imported, so it only participates if tsconfig `files` or `include` matches it, and an augmentation inside a `.ts` file only applies if something imports that file. Confirm the file is in the program, then confirm you named the module that actually *declares* the interface rather than a re-exporting entry point.
saying these in an interview costs you the question
- Copies the package's .d.ts into the repo and edits it
- Writes the augmentation in a file with no imports or exports
- Thinks augmentation can override an existing property's type
- Declares the added property required when middleware may not run
- Assumes the declaration file applies without being in the program