Have strong expressions.

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!

Daniel Novacik

Fullstack developer @ Panaxeo
Orava man
Fancy cat person

https://www.linkedin.com/in/novacikdaniel/
Previous
Previous

Quality issues? Focus on this.

Next
Next

A child and a half and counting