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"));
});
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)
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;
}
}
... 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;
}
}
Top comments (7)
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 theexport Request
approach, but that broke the middleware I was using. This is the only approach I've seen that works. THANK YOU!Glad it helped! You're welcome.
We discussed this in discord recently, so a few notes from that:
Firstly, regarding the
Request
, the type declarations forexpress
actually contain two different interfaces:Express
namespace with aRequest
interface. It also mentions that this is what should be extendedexpress-serve-static-core/index.d.ts
also exports anRequest
interface which extends from the otherRequest
interface mentioned above.To illustrate the difference:
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):
Note:
declare global
only works in module files (must contain at least one import / export). Justdeclare 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 thedeclare global
and just usedeclare 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 inexpress-serve-static-core
Secondly, regarding the
Session
, there are at least three 3 different session types we need to consider to understand this:Session
class that does not extend anythingexpress-session/index.d.ts
exports aSessionData
interfaceexpress-session/index.d.ts
also extends theExpress.Request
interface with asession
property of typesession.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:It's crucial here that the
.d.ts
file is a module file, not a global declaration file, otherwise you'll end up overwrite theSessionData
interface instead (so e.g. it won't have acookie
property anymore), which may result in errors. You can make sure it is considered a module file either by importing fromexpress-session
or just by adding aexport {}
statement to the of the file.Then you can access the new property like this
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.
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.
Did you ever figure this out? I'm having the same problem.
This is a great piece! Thank you.
Do you understand why we must extend
express-serve-static-core
instead ofexpress
?