2021-12-22
|~3 min read
|445 words
One of Express.js’s strengths is how easy it is to write middleware. However, the first time you try to do so with the type bindings for Typescript you might run into some issues.
For example, imagine the middleware function that decodes a JWT and looks up a user in a database. If that user exists, we add them to the request. If they don’t, we respond with a 401 Unauthorized
.
One way to write that would look like the following:
import { Request, Response, NextFunction } from "express"
import { verifyToken } from "../../utils"
import { fetchUserById } from "../users/user.service"
export const protect = async (
req: Request,
res: Response,
next: NextFunction,
) => {
const bearer = req?.headers?.authorization
if (!bearer || !bearer.startsWith("Bearer ")) {
return res.status(401).end()
}
const token = bearer.split("Bearer ")[1].trim()
let payload
try {
payload = await verifyToken(token)
} catch (e) {
return res.status(401).end()
}
const user = await fetchUserById(payload.id)
if (!user) {
return res.status(401).end()
}
req.user = user // Compilation error
next()
}
The problem is that Typescript will complain about this as there’s no user
property on the Request
type.
And, while the type for Request
accepts generics, none of them allow you to augment the request itself.
So what can we do? We can use module augmentation, a form of declaration merging!
import { Request, Response, NextFunction } from 'express'
import { verifyToken } from '../../utils'
+ import { User } from '../users'
import { fetchUserById } from '../users/user.service'
+ declare module 'express' {
+ export interface Request {
+ user?: User
+ }
+ }
+
+ const payloadHasId = (payload: unknown): payload is { id: string } => {
+ return Boolean(payload && typeof payload === 'object' && 'id' in payload)
+ }
+
export const protect = async (
req: Request,
res: Response,
next: NextFunction,
) => {
const bearer = req?.headers?.authorization
if (!bearer || !bearer.startsWith('Bearer ')) {
return res.status(401).end()
}
const token = bearer.split('Bearer ')[1].trim()
let payload
try {
payload = await verifyToken(token)
+ if (!payloadHasId(payload)) {
+ throw new Error('Token missing required information')
+ }
} catch (e) {
return res.status(401).end()
}
const user = await fetchUserById(payload.id)
if (!user) {
return res.status(401).end()
}
req.user = user
next()
}
By declaring a new definition for the module "express"
we can extend the interface for Request
as we need. In this particular case, we needed user
as type User
(which we define in the app), but it could have been anything.
The other great part is that because Typescript is aware of this new merged definition, we are able to access the user
property on any Request
typed variables throughout the app.
Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!