Solving this with .d.ts declarations are hacks. Accept the fact that express' middleware system is not for typescript. I suggest not to use them.
If you use .d.ts, you lose compile time checks. You might expect that something is there, even if it is not. Or you can define it as optional, but then you have to check every time whether it is there or not. We use typescript to catch the errors at compile time, but .d.ts files doesn't help us. They can't be a solution.
Example express middleware (NOT recommended):
const auth = (req, res) => {
const user = // parse user from the header
if(!user)
return res.status(401).send({ result: 'unauthorized-error' })
req.user = user
return next()
}
app.get('/something', auth, (req, res) => {
// do something
})
Better code:
const auth = (req) => {
return // parse user from the header
}
app.get('/something', (req, res) => {
const user = auth(req)
if(!user)
return res.status(401).send({ result: 'unauthorized-error' })
// do something
})
You can get your middleware like usage back with higher order functions:
const auth = (req) => {
return // parse user from the header
}
const withUser = (callback: (foo, req, res) => void) => (req, res) => {
const user = auth(req)
if(!user)
return res.status(401).send({ result: 'unauthorized-error' })
return callback(user, req, res)
}
app.get('/something', withUser((user, req, res) => {
// do something
}))
You can even stack them up if you want:
const withFoo = (callback) => (req, res) => { /* ... */ }
const withBar = (callback) => (req, res) => { /* ... */ }
const withBaz = (callback) => (req, res) => { /* ... */ }
const withFooBarAndBaz = (callback) => (req,res) => {
withFoo((foo) =>
withBar((bar) =>
withBaz((baz) =>
callback({ foo, bar, baz }, req, res)
)(req,res)
)(req,res)
)(req,res)
}
app.get('/something', withFooBarAndBaz(({ foo, bar, baz }, req, res) => {
// do something with foo, bar and baz
}))
You can use the language instead of unsafe mutations that express promotes.
Before my current solution, I used the first proposed way to achieve middlewares, but with the difference that my auth function threw an error, which I could catch and return a correct response, so I didn't have to do it in the controller. For example:
app.get('/something', withResponse((req) => {
const user = auth(req)
return success({
message: `Hi ${user.name}`
})
}))
I also returned the responses instead of manually calling res.send. It allowed me to type the responses as well.
My current solution goes much further, where I can auto infer every return type which I can use on the frontend immediately. For this, Check tRPC.
If you want to keep your REST-like API structure, with extra type-safety guarantee, check out my tRPC alternative: Cuple RPC