I am writing a user auth middleware based on Jason Watmore's user auth boilerplate.
My expected result is that this code "just works" because it's copied from another project where it does exactly that.
My actual result is that I get this long error message:
(alias) authorize(roles?: Role[]): ({
(req: express.Request<ParamsDictionary, any, any, QueryString.ParsedQs, Record<string, any>>, res: express.Response<...>, next: express.NextFunction): Promise<...>;
unless: (options: Params) => {
...;
};
} | ((request: RequestWithUser, res: express.Response<...>, next: express.NextFunction) => Promise<...>))[]
import authorize
No overload matches this call.
The last overload gave the following error.
Argument of type '({ (req: Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>, res: Response<any, Record<string, any>>, next: NextFunction): Promise<...>; unless: (options: Params) => { ...; }; } | ((request: RequestWithUser, res: Response<...>, next: NextFunction) => Promise<...>))[]' is not assignable to parameter of type 'RequestHandlerParams<ParamsDictionary, any, any, ParsedQs, Record<string, any>>'.
Type '({ (req: Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>, res: Response<any, Record<string, any>>, next: NextFunction): Promise<...>; unless: (options: Params) => { ...; }; } | ((request: RequestWithUser, res: Response<...>, next: NextFunction) => Promise<...>))[]' is not assignable to type '(ErrorRequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>> | RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<...>>)[]'.
Type '{ (req: Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>, res: Response<any, Record<string, any>>, next: NextFunction): Promise<...>; unless: (options: Params) => { ...; }; } | ((request: RequestWithUser, res: Response<...>, next: NextFunction) => Promise<...>)' is not assignable to type 'ErrorRequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>> | RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<...>>'.
Type '(request: RequestWithUser, res: Response, next: NextFunction) => Promise<Response<any, Record<string, any>> | undefined>' is not assignable to type 'ErrorRequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>> | RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<...>>'.
Type '(request: RequestWithUser, res: Response, next: NextFunction) => Promise<Response<any, Record<string, any>> | undefined>' is not assignable to type 'ErrorRequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>'.
Types of parameters 'res' and 'req' are incompatible.
Type 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>' is missing the following properties from type 'Response<any, Record<string, any>>': status, sendStatus, links, send, and 53 more.ts(2769)
index.d.ts(163, 5): The last overload is declared here.
No quick fixes available
Here is the offending code
function authorize(roles: Role[] = []) {
if (typeof roles === "string") {
roles = [roles];
}
return [
jwt({
secret, // authenticate JWT token and attach user to request object (req.user)
algorithms: ["HS256"],
}),
// problem is caused here with "RequestWithUser"
async (request: RequestWithUser, res: Response, next: NextFunction) => {
const acctInfo = request.auth;
if (acctInfo?.acctId === undefined) {
return res.status(401).json({ message: "Unauthorized" });
}
request.user = {
acctId: acctInfo.acctId,
role: "", // temp to satisfy ts
};
const account: Account | null = await acctDAO.getAccountById(acctInfo.acctId);
if (!account) return res.status(401).json({ message: "Unauthorized" });
const refreshTokens = await rtDAO.getAllRefreshTokensForAccount(account.acctId);
const validRoles = Object.values(Role);
const acctRole: Role = account.role as Role;
const rolesFoundOnRequest = roles.length;
if (rolesFoundOnRequest && !validRoles.includes(acctRole)) {
return res.status(401).json({ message: "Unauthorized" });
}
request.user.role = account.role;
request.user.ownsToken = (token: string) => !!refreshTokens.find((x: any) => x.token === token);
next();
},
];
}
export default authorize;
When I change RequestWithUser
back to just Request
, the error goes away, but that doesn't work because then the rest of the middleware will have TS errors about the wrong type being expected. so it has to be this way.
As you can see, RequestWithUser
is just an Express Request
extended:
export interface RequestWithUser extends Request {
user?: {
role: string;
ownsToken?: Function;
acctId: number;
};
auth?: {
sub: any;
acctId: number;
};
}
I don't understand this error message at all. It seems to be saying "express will pass this to ErrorRequestHandler
and the shapes don't fit" but I'm not clear at all what's up.
edit: so obviously I should tell you where the error appears. The following routes are all examples of where it appears.
this.router.get("/", authorize([Role.Admin]), this.getAllAccounts);
this.router.get("/:id", authorize(), this.getAccountById);
this.router.post("/", authorize([Role.Admin]), createAccountSchema, this.createAccount);
this.router.put("/:id", authorize(), updateRoleSchema, this.updateAccount);
RequestWithUser
is just an ExpressRequest
extended
Instead of creating a specific interface (even if it extends Request
), use declaration merging to augment the actual Request
interface itself. See e.g. Extend Express Request object using Typescript
The issue is not in the runtime code, but just on the extra typing.
The error message itself is indeed misleading, but it is directly caused by the different 1st parameter type.
Because it does not match the usual Request
type, TypeScript tries to infer a different function overload, and ends up with one that should accept a mix of normal RequestHandler and ErrorRequestHandler... but your functions cannot handle both, hence the error message.