Search code examples
typescriptexpresssessionpassport.jsexpress-session

How to make Express with Typescript and Passport "user" required after middleware?


I have to keep doing stuff like

if (!req.user) return res.status(401).send()

The approach the comes into mind would be an express middleware for that. But even tho I can prevent non-logged users from reaching the route I can't think of a way to type express req correctly.

If I override the "req" param the router complains because the user key in "Express.User" is an optional parameter.

I don't believe changing the global override so "user" is required is a good option since "user" should only be required after the middleware validation. How can I achieve this?

Below some piece of useful code to understand the context.

Global Express Override

declare global {
  namespace Express {
    interface User extends TUser {
      _id: ObjectId
    }
  }
}

What I want to achieve

// Router
router.post('/', sessionMiddleware, controller)

// Middleware
const sessionMiddleware = (req: Request, res: Response, next: NextFunction) => {
if (!req.user) return res.status(401).send()
next()
}

// Controller
const controller = (req: RequestWithRequiredUser, res: Response) => {
  // user here can't possibly be typed undefined
  const user = req.user
  ...
}

What I actually have to do everytime:

const controller = (req: Request, res: Response) => {
  if (!req.user) return res.status(401).send()

  ...doSomethingElse
}

Solution

  • I don't if better solution with middlewares are available but this is a workaround I found to avoid repeating logic.

    buildSessionController.ts

    /** Make given keys required */
    export type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] }
    
    type TController<T extends Request> = (req: WithRequired<T, 'user'>, res: Response) => unknown
    
    export const buildSessionController =
      <T extends Request>(controller: TController<T>) =>
      (req: Request, res: Response) => {
        if (!req.user) return res.status(401).send()
    
        // This is needed as a type workaround because a type mismatch happens otherwise
        // Using Omit instead of WithRequired above doesn't fix it
        const request = req as WithRequired<T, 'user'>
    
        return controller(request, res)
      }
    
    

    someController.ts

    export const expressController = 
      buildSessionController((req, res) => {
      
        // user will never be undefined here
        const { user } = req
    
        return res.json(user)
      })