Have strong expressions.
In Node.js. A Frontend techtip.
Who wouldn’t want to use strongly typed TypeScript code in their Node.js Express app? Turns out it might be a little tricky, especially when trying to define routes or request and response objects.
But worry not, there is of course a way!
Welcome to Panaxeo Tech Tips. Our monthly section with tips from our Club-Frontend members and more!
It would be easiest to extend the Express.Request and Express.Response types, right? Sure, but you might not get proper API responses regarding TypeDoc documentation — mostly because of method chaining, aka res.status(200).json() — this would use the original json() function.
What you could do is create a new type, like this:
export type TypedResponse<T> = Omit<Response, 'json' | 'status'> & { json(data: T): TypedResponse<T>, status(code: number): TypedResponse<T> }
The omit part removes the original implementation of json and status methods and replaces it with your typed version.
Afterwards, the usage is simple:
const authenticate = (req: Request, res: TypedResponse<AuthResponse | IBaseResponse>, next: NextFunction) => {
authService.authenticate(req.body)
.then(user => user
? res.json({ success: true, ...user })
: res.status(401).json({ success: false, message: 'Incorrect credentials provided' }))
.catch(err => next(err))
}
These are the models we’ve used:
// our models
export interface AuthResponse extends IBaseResponse {
name: string
email: string
token: string
}
export interface IBaseResponse {
success: boolean
message?: string
}
That’s a typed response with all the props on our T type. Yippie!
Now, what about the request types? This is a little trickier, as express uses its types (the Query, to be precise) from a package express-static-serve-core
, which the express itself doesn’t expose, so we need this package to be able to extend our requests with our own Types.
It can be achieved like this:
import { Query } from 'express-serve-static-core'
export interface TypedRequest<T extends Query, U> extends Express.Request {
body: U
query: T
}
And now that we have both the body AND the query strongly typed, the usage is pretty straightforward:
const updateUserTokenData = (req: TypedRequest<{ id: string}, { token: string, subId: string }>, res: TypedResponse<IBaseResponse>, next: NextFunction) => {
userService.updateUserTokenData(req.query.id, { ...req.body })
.then(updatedToken => updatedToken
? res.json({ success: true })
: res.status(401).json({ success: false, message: "Your error reason" }))
.catch(err => next(err))
}
Both the requests and the responses are now strongly typed, our intellisense works as expected, and TypeScript will inform us if we’re not typing the correct properties.
PS: Don’t forget to validate the payload sent by the user ;)
And that’s all folks!
Found this little tip useful?
Maybe you’ve got some questions of your own, or maybe there’s a tip you’d like to share.
Let us know, our #club-frontend would love to hear it!