2
votes

How to extend and use an extended Request type in Typescript / Express?

I'm adding a BUNCH of middleware libraries that extend the Request Object. I.E. One added user to req. Another add cookie() to req. Another add csrfToken() to req. etc..

When I add a request handler function, how do I tell that function to use the req with all the bells and whistles added by middleware?

Do I hunt down every DefinitelyTyped package corresponding to the middleware? If so, then will the Request type be magically 'decorated` with these properties?

To make it even harder, I've written my own middleware that adds properties to Request

req.myCustomFunction()

In this case, will I need to declare, and extend the Request myself with myCustomFunction?

In addition, will that Request which I am extending 'include' the types given by DefinitelyTyped?

declare namespace Express {
  export interface Request {
    myCustomFunction: () => void
  }
}

Will this now include ALL the properties included via DefinitelyTyped AND my myCustomFunction?

How do I reference this interface when using?

Will it be Express.Request? Or just Request?

If I reference it as Request, how does Typescript know to use "my" Request and not the one exported by Express's DefinitelyTyped library?

2

2 Answers

3
votes

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 exported (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:

global augmentation adds property to Express.Request and appears in completions

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.

0
votes

I created a @types folder in my project and added the following code:

@types/express/index.d.ts

import { Express } from "express-serve-static-core";

declare module "express-serve-static-core" {
    interface Request {
        ... custom properties and methods here ...
    }
}

This allowed me to extend the Request type with whatever properties and methods I wanted