James Craig
James Craig

Reputation: 6864

TypeScript: Custom Request Types on Custom Express Router (TS2769)

I'm having difficulty getting custom Request types to play nicely with TypeScript.

In my app there are public and private routes.

The public routes use the Request type from Express. The private routes use a custom PrivateRequest type which extends the Request type from Express; which looks like this:

import type { Request } from "express";
import type * as Cookies from "cookies";

export type PrivateRequest = Request & {
  user: User;
  cookies: Cookies;
}

Routing for the public and private routes looks like this:

const publicRouter = express.Router();
const privateRouter = express.Router();

privateRouter.use([userSession]);

publicRouter.post("/login", login);
privateRouter.get("/api/user", user);

Here's an example of a private route, which makes use of the PrivateRequest type and there are no problems with TypeScript here.

export default async function user(req: PrivateRequest, res: Response) {
  try {
    res.json({ test: true });
  } catch (err) {
    console.error(err);
    res.status(500).json({ errors: { server: "Server error" } });
  }
}

The issue is with the private routes, e.g.:

privateRouter.get("/api/user", user);

The specific error I get back from TypeScript for the privately defined routes is this:

TS2769: No overload matches this call

How can I fix this? Everything I try doesn't work, and I'm not entirely sure why.

I can fix this error if I make user nullable on the PrivateRequest but this is technically incorrect, since all of the private routes are guaranteed as user on the req object since the userSession middleware either responds with a 401 or adds the user to the req object for subsequent private routes. Here's an exmaple of whay my userSession middleware looks like:

export default async function userSession(
  req: Request,
  res: Response,
  next: NextFunction
) {
  try {
    req.cookies = new Cookies(req, res);

    // [...] authentication and user entity fetching (throws if either one fails)

    if (user === null) {
      res.status(401).json({ errors: { server: "Unauthorised" } });
    } else {
      // @ts-ignore
      req.user = user;
      next();
    }
  } catch {
    res.status(401).json({ errors: { server: "Unauthorised" } });
  }
}

Upvotes: 4

Views: 970

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1075875

Since Express's get is defined to accept handlers accepting Request, not PrivateRequest, you'll have to reassure TypeScript that this is okay, that you know that user and cookies will be added to the request object. The way get is defined, TypeScript has to assume that your handler will just get a Request, not a PrivateRequest.

Here are two approaches for you:

  1. Use a type assertion, probably in your own get-like utility function you'd use only for private routes:

    const addPrivateGet = (path: string, handler: (req: PrivateRequest, res: Response)) => {
        privateRouter.get(path, handler as unknown as (req: Request, res: Response) => void);
    };
    // ...
    addPrivateGet("/api/user", user);
    
  2. Alternatively, use an assertion function in your private route handlers. Here's the assertion function:

    function assertIsPrivateRequest(req: Request): asserts req is PrivateRequest {
        if (!("user" in req)) {
            throw new AssertionError(`Invalid request object, missing 'user'`);
        }
        if (!("cookies" in req)) {
            throw new AssertionError(`Invalid request object, missing 'cookies'`);
        }
    }
    

    Here's an example using it in a private route:

    export default async function user(req: Request, res: Response) {
    // Note −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−^
      try {
        assertIsPrivateRequest(req); // <===== Narrows `req` to `PrivateRequest`
        // Now you can use `req.user` and `req.cookies` here
        res.json({ test: true });
      } catch (err) {
        console.error(err);
        res.status(500).json({ errors: { server: "Server error" } });
      }
    }
    

    The assertion function both narrows req to PrivateRequest and also provides a handy runtime check so you get a proactive error if you use it in a public route by mistake (or there's some problem with the middleware and your additional properties aren't added). (If you don't like the overhead of the in operations, you could do them only in development and staging but not production.)


It doesn't apply to your case where you want Request itself to be unchanged (for public routes) and to have a separate extended PrivateRequest type (for private routes), but for others who may find this answer: If you wanted to directly augment Request so the augmentations applied everywhere in your code, you could do that with declaration merging. To do that, have a file in your project that augments the interface, like this:

// The types for Express use `e` as their namespace
declare module e {
    interface Request {
        someNewProperty: string;
    }
}

I think traditionally you'd name that file with a .d.ts extension (for instance, express.augments.d.ts), but you don't have to (and I could be mistaken).

That code adds someNewProperty directly to Request, globally, so this works:

app.get("/", (req, res) => {
    console.log(req.someNewProperty);
    // ...
});

This augmentation just does the type part of it, of course; it's up to your code to use middleware or similar to add the property so the type is accurate.

Upvotes: 4

Related Questions