DEV Community

Chris927
Chris927

Posted on

Extending Express' Types with TypeScript Declaration Merging - TypeScript 4

TypeScript is evolving fast (as are a lot of tools in the Open Source space, gladly!)... but this means that what worked in a previous version may not work the same any more in the next major release. This happened to me in this case with TypeScript 4 and declaration merging.

There are good articles out there (like this one, thanks, Kwabena!), but it's slightly different in TypeScript 4 (and with modern typescript-eslint rules).

Sounds like your issue? Read on (or jump straight to the code example below).

To keep it simple, let's imagine we have some middleware (e.g. passport) that makes the current user available on each request, in the form of a userId (which may be of type string).

On some route or other middleware, we now want to access the userId like this:

app.get("/some-route", (req: Request, res: Response) => {
  res.send("Hello, world! Your userId is " + (req.userId || "not available"));
});
Enter fullscreen mode Exit fullscreen mode

TypeScript won't be happy with this, though. We will get an error like this:

Property 'userId' does not exist on type 'Request<ParamsDictionary, any, any, ParsedQs>'.ts(2339)
Enter fullscreen mode Exit fullscreen mode

We need to tell the Request interface that there is a userId property. But how?

The TypeScript 3 way of solving this (using declare global) may still work, but it would give me a warning, due to an eslint rule, which basically states that declare global is the old and outdated way.

The new way is to use declare module. In our example we can therefore introduce the userId to Express' Request type like this:

declare module "express-serve-static-core" {
  interface Request {
    userId?: string;
  }
}
Enter fullscreen mode Exit fullscreen mode

... and voila, the warning is gone, and (more importantly) type safety through TypeScript is restored.

(It isn't overly intuitive that the Request type must be extended in the module express-serve-static-core...)

Now, what if you add a field to the session (assuming you are using express-ession)? The additional field needs to be declared for the Session type, inside the express-session module, like this:

declare module "express-session" {
  interface Session {
    someSessionVar: string;
  }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (7)

Collapse
 
voinik profile image
voinik

My god, you are a life saver! I've been looking for a way to do this. I came across the declare global approach, but that didn't work. I came across the export Request approach, but that broke the middleware I was using. This is the only approach I've seen that works. THANK YOU!

Collapse
 
chris927 profile image
Chris927

Glad it helped! You're welcome.

Collapse
 
blutorange profile image
blutorange • Edited

We discussed this in discord recently, so a few notes from that:

Firstly, regarding the Request, the type declarations for express actually contain two different interfaces:

To illustrate the difference:

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

declare global {
  namespace Express {
    interface Request {
      authInfo?: string | undefined;
    }
  }
}

declare module "express-serve-static-core" {
  interface Request {
    userId?: string;
  }
}

declare const x: Express.Request;
declare const y: Request;

x.authInfo; // works
x.userId; // error: Property 'userId' does not exist on type 'Request'

y.authInfo; // works
y.userId; // works

export { };
Enter fullscreen mode Exit fullscreen mode

Playground link

So the first thing to note is that the two approaches to extending the Request interface are not at all equivalent. Extending the request from the global namespace will add the property to that interface and also to the other request interface.

And according to the source code, this is the officially endorsed manner of extending it, so this is what should be used (ignore the ESLint error, this is something the authors of express and/or its types should heed and change):

declare global {
    namespace Express {
        interface Request {
            authInfo?: string | undefined;
        }
    }
}
export {}
Enter fullscreen mode Exit fullscreen mode

Note: declare global only works in module files (must contain at least one import / export). Just declare global { .. } in a .d.ts file will result in an error. If you don't have any imports / exports, the file is a global file and you should omit the declare global and just use declare namespace Express { ... }.

As to why this lead to errors with middleware for some users in the comments, it's hard to tell without seeing the actual example and error; perhaps it was due what I mentioned in the paragraph above; or it may have been due to a faulty configuration that resulted in TS not including the declarations in the compilation; or the middleware's typings may have been wrong; or perhaps a non-optional property was added that resulted in a type conflict (and was avoided by extending the "wrong" interface?).

And since somebody had been asking why we cannot extend the express module: that's simply because the types are not declared in that module, but in express-serve-static-core


Secondly, regarding the Session, there are at least three 3 different session types we need to consider to understand this:

  • express-session/index.d.ts exports a Session class that does not extend anything
  • express-session/index.d.ts exports a SessionData interface
  • express-session/index.d.ts also extends the Express.Request interface with a session property of type session.Session & Partial<session.SessionData>

And checking with the source code again, you should not directly extend the Session class, rather, you should extend the SessionData interface:

// file: types.d.ts
// THIS LINE IS REQUIRED
import { SessionData } from "express-session";

declare module "express-session" {
    export interface SessionData {
        foobar?: string;
    }
}
Enter fullscreen mode Exit fullscreen mode

It's crucial here that the .d.ts file is a module file, not a global declaration file, otherwise you'll end up overwrite the SessionData interface instead (so e.g. it won't have a cookie property anymore), which may result in errors. You can make sure it is considered a module file either by importing from express-session or just by adding a export {} statement to the of the file.

Then you can access the new property like this

// file: index.ts
import { Request } from "express";

declare const r: Request;
r.session.cookie; // works
r.session.foobar;
Enter fullscreen mode Exit fullscreen mode
Collapse
 
philipphock profile image
philipphock

This is very short and easily explained, but where do you save this. You can, of course, write it in every file you are using a session, but obviously that's not how you would do it. You probably want to use some sort of d.ts file. Could you explain how to use them properly.

Collapse
 
actlikewill profile image
Wilson

Hello did you figure this out? I have the same issue. I added a @types file at the root of my project with a global.d.types file. in there I added the above code. I also updated my tsconfig.json typeRoots entry to point to the above folder as well as node modules type folder. Within the editor I see that the types are being recognised but the compiler keeps throwing an error.
Did you make any progress on this? Any help is appreciated.

Collapse
 
otobot1 profile image
otobot1

Did you ever figure this out? I'm having the same problem.

Collapse
 
timrohrer profile image
tim.rohrer

This is a great piece! Thank you.

Do you understand why we must extend express-serve-static-core instead of express?