The easy part
Do I hunt down every DefinitelyTyped package corresponding to the middleware?
Yes, you should install type definitions for everything you install, but no, there shouldn’t be any hunting to do. When you install a library from npm, say
npm install express-ntlm
you can follow up by attempting to install types for it:
npm install @types/express-ntlm
If the package exists on DefinitelyTyped, that will be it. If it doesn’t (because it ships its own types or because nobody has written types for it), npm will give you a 404, and you can move on.
If so, then will the Request
type be magically 'decorated` with these properties?
Yes, that’s the idea. If a middleware is supposed to augment Request
objects but the typings don’t do this, they are wrong. If it’s a popular library, they won’t stay wrong for long. Someone is likely to submit a PR to DefinitelyTyped fixing it.
The harder part
To answer the rest of your questions in a way that will stick, you need to have a basic understanding of declaration merging. It also helps to understand the difference between modules and scripts.
Declaration merging
In TypeScript, some kinds of declarations with the same name are allowed to merge. In particular, interfaces are allowed to merge with interfaces, and namespaces are allowed to merge with namespaces. This means you can split them up into multiple separate locations:
interface Cat {
meow(): Sound;
}
interface Cat {
name: string;
}
namespace Express {
interface Request {}
}
namespace Express {
interface Response {}
}
function doSomethingWithCat(cat: Cat) {
cat.name; // string
cat.meow(); // Sound
}
let req: Express.Request;
let res: Express.Response;
The multiple declarations of Cat
are merged together, and you can use it as if it were one uniform interface. The same is true of Express
. This even works across files, and it works with things nested within interfaces too:
// File: a.ts
namespace Express {
interface Request {}
}
// File: b.ts
// If I want to add a property to `Express.Request` in a.ts, I have to merge
// both the namespace and the interface:
namespace Express {
interface Request {
myCustomFunction(): void;
}
}
Modules vs. Scripts
If a file contains an import
or export
, it is a module. If not, TypeScript considers it a script. Modules have their own scope, which means top-level declarations in one module can’t be accessed in another module unless they are export
ed (which is kind of the whole point). Scripts are global, so any top-level declarations in one script are accessible to other scripts.
The tricky thing here is that these remarks apply not just to variables and functions, but to types and interfaces, and they also apply in type declaration files (.d.ts
) inside your node_modules
, not just in the app files you write yourself.
This is important because it can affect how declaration merging across files works. When I said that interfaces can merge across files, it takes a little more work to do this when one or both files are modules, because they are isolated by default. Let’s revisit the previous example with a.ts
and b.ts
, but this time, we will make b.ts
a module:
// File: a.ts
namespace Express {
interface Request {}
}
// File: b.ts
import express from 'express';
// Oops, this only creates a *local* declaration
// called Express. It doesn’t actually merge with a.ts,
// because I’m in a module scope here.
namespace Express {
interface Request {
myCustomFunction(): void;
}
}
Our declaration merging has stopped working, because we’re declaring Express
in two completely different scopes: the global scope, and b.ts’s module scope. We need a way to “escape” the module scope from b.ts:
// File: b.ts
import express from 'express';
// Now it merges with Express.Request in a.ts!
declare global {
namespace Express {
interface Request {
myCustomFunction(): void;
}
}
}
Putting it all together
In this case, will I need to declare, and extend the Request
myself with myCustomFunction
?
Yes, it looks like you’ve already got this part down. The snippet you wrote looks correct if it occurs in a script. If the file where you wrote it has an import
or an export
, it won’t work anymore, and you’ll need to wrap it in declare global
. The reason this works is @types/express-serve-static-core
, which is automatically included by @types/express
, sets Express.Request
up for you to merge with. Then, they extend that base type with all the built-in express stuff (get
, header
, param
, etc.) and reference that type throughout the rest of their definitions. (I will admit that it would be pretty difficult to determine that Express.Request
was there and ready for you to extend if nobody had told you it was there, but it looks like you figured it out before coming here.)
In addition, will that Request
which I am extending 'include' the types given by DefinitelyTyped?
Now that you know about declaration merging and have seen what you’re merging with, you can see that the answer is technically no: you’re merging with an empty interface, so Express.Request
will include what you put on it and what other middleware typings put on it, but not the core express stuff. But that doesn’t matter, because the type of req
in a route handler extends Express.Request
, so at that point, the answer is yes, that type should contain everything from the core express typings, all your middleware typings, and your own custom augmentations:

How do I reference this interface when using? Will it be Express.Request
? Or just Request
?
As we saw, Express.Request
, which is available as a global, will contain only augmentations, not the core express stuff. The complete Request
type is exported from the express
package, so you can reference it like:
import express from 'express';
// Or, depending on your compiler settings:
import * as express from 'express';
// Or yet again:
import express = require('express');
function doSomethingWithRequest(req: express.Request) { ... }
or
import { Request } from 'express';
But the best way is usually through no explicit reference at all:
import express from 'express';
const app = express();
app.get('/', req => {
req.myCustomFunc(); // 'req' is contextually typed by `app.get`, and has what you want
});
(Confusingly, the global Request
type is something completely unrelated to express.)
If I reference it as Request
, how does Typescript know to use "my" Request
and not the one exported by Express's DefinitelyTyped library?
Because you’ve learned about declaration merging, you now know that this is an empty question: your declaration merged with the one in the DefinitelyTyped package to create one Request
. (The fact that the exported Request
is a separate type that extends the global Express.Request
is an unfortunate distraction from this simple truth.) Because they have merged, you could not reference them separately if you wanted to.