Search code examples
typescriptexpresstypescript-types

Cannot extend express request


I'm trying to extend the Request interface of Express as:

import express, { Request, Response } from 'express';
interface IRequest extends Request {
  user: {
    id: string;
  }
}

const router = express.Router();

router.get('/', auth, async (req: IRequest, res: Response) => {
  try {
    const user = await User.findById(req.user.id).select('-password');
    res.json(user);
  } catch (e) {
    console.error((e as Error).message);
    res.status(500).send('Server Error');
  }
});

but I got the following error:

No overload matches this call. Overload 1 of 3, '(path: PathParams, ...handlers: RequestHandler<ParamsDictionary, any, any, ParsedQs>[]): Router', gave the following error. Argument of type '(req: IRequest, res: Response) => Promise' is not assignable to parameter of type 'RequestHandler<ParamsDictionary, any, any, ParsedQs>'. Types of parameters 'req' and 'req' are incompatible. Property 'user' is missing in type 'Request<ParamsDictionary, any, any, ParsedQs>' but required in type 'IRequest'. Overload 2 of 3, '(path: PathParams, ...handlers: RequestHandlerParams<ParamsDictionary, any, any, ParsedQs>[]): Router', gave the following error. Argument of type '(req: IRequest, res: Response) => Promise' is not assignable to parameter of type 'RequestHandlerParams<ParamsDictionary, any, any, ParsedQs>'. Type '(req: IRequest, res: Response) => Promise' is not assignable to type 'RequestHandler<ParamsDictionary, any, any, ParsedQs>'.ts(2769)


Solution

  • Typescript will not let you, because even if you typed it this way:

    router.get('/', auth, async (req: IRequest, res: Response) => {
    

    Typescript will still just assume that express will emit a Request and not an IRequest.

    However, since you are probably monkey-patching the request object, you can use declaration merging to basically augment's Express' internal request object:

    https://www.typescriptlang.org/docs/handbook/declaration-merging.html

    If you don't want to globally make this change to Request, you could also use a assertion to make sure that it really was an IRequest:

    router.get('/', auth, async (req: request, res: Response) => {
      try {
        assertIRequest(req);
        const user = await User.findById(req.user.id).select('-password');
        res.json(user);
      } catch (e) {
        console.error((e as Error).message);
        res.status(500).send('Server Error');
      }
    });
    
    function assertIRequest(req: Request|IRequest): asserts req is IRequest {
      if (!req?.user?.id) throw new Error('Request was not an IRequest');
    }