I’m using Nextjs for a project and I’ve created the following interface to decorate other functions:
import type { NextApiRequest, NextApiResponse, NextApiHandler } from 'next';
interface ApiHandlerDecorator<T, S> {
<Request extends NextApiRequest, Response extends NextApiResponse>(
handler: (
request: Request & T,
response: Response | NextApiResponse<S>
) => ReturnType<NextApiHandler>
): (
request: Request & T,
response: Response | NextApiResponse<S>
) => ReturnType<NextApiHandler>;
}
export default ApiHandlerDecorator;
I have two decorators:
import { verify } from 'jsonwebtoken';
import type { ApiHandlerDecorator } from './interfaces';
import {
BadRequestError,
ExpiredCredentialsError,
InvalidCredentialsError,
MissingCredentialsError,
UnexpectedError,
} from './pages/api/errors';
type AuthorizationValidationErrors =
| MissingCredentialsError
| InvalidCredentialsError
| ExpiredCredentialsError
| UnexpectedError
| BadRequestError
| undefined;
const authorizationValidation: ApiHandlerDecorator<
{ headers: { authorization: string } },
AuthorizationValidationErrors
> = (handler) => async (request, response) => {
const {
headers: { authorization },
} = request;
let statusCode = 401;
let json: AuthorizationValidationErrors = new MissingCredentialsError(
'Missing authorization header'
);
if (authorization) {
try {
const decodedJwt = verify(authorization, process.env.SECRET_KEY);
if (
decodedJwt &&
typeof decodedJwt === 'object' &&
'roomId' in decodedJwt &&
'playerId' in decodedJwt
) {
await handler(
{ ...request, ...{ headers: { authorization } } },
response
);
} else {
statusCode = 400;
json = new BadRequestError(
'Malformed authorization. Missing roomId and playerId.'
);
}
} catch ({ name: errorName }) {
if (errorName === 'JsonWebTokenError') {
statusCode = 401;
json = new InvalidCredentialsError(
'Invalid authorization. Authorization not signed.'
);
} else if (errorName === 'TokenExpiredError') {
statusCode = 403;
json = new ExpiredCredentialsError(
'Expired authorization. Refresh authorization.'
);
} else {
statusCode = 500;
json = new UnexpectedError(
'Unexpected error while processing authorization'
);
}
}
} else {
response.status(statusCode).json(json);
}
};
export default authorizationValidation;
and
import type { ApiHandlerDecorator } from './interfaces';
import { decode } from 'jsonwebtoken';
import { BadRequestError, MissingCredentialsError } from './pages/api/errors';
type RoomAndPlayerIdInjectionErrors =
| BadRequestError
| MissingCredentialsError
| undefined;
const roomAndPlayerIdInjection: ApiHandlerDecorator<
{ roomId: string; playerId: string },
RoomAndPlayerIdInjectionErrors
> = (handler) => async (request, response) => {
const {
headers: { authorization },
} = request;
if (authorization) {
const decodedJwt = decode(authorization);
if (
decodedJwt &&
typeof decodedJwt === 'object' &&
'roomId' in decodedJwt &&
'playerId' in decodedJwt
) {
await handler(
{
...request,
roomId: decodedJwt.roomId,
playerId: decodedJwt.playerId,
},
response
);
} else {
response
.status(400)
.json(
new BadRequestError(
'Malformed authorization. Missing roomId and playerId.'
)
);
}
} else {
response
.status(401)
.json(
new MissingCredentialsError(
'Missing authorization. Authorization header not provided.'
)
);
}
};
export default roomAndPlayerIdInjection;
When I decorate a function like this:
const handler = authorizationValidation(
roomAndPlayerIdInjection(async (request, response) => {
// request.headers.authorization is string | undefined
});
);
If I change call order and do:
const handler = roomAndPlayerIdInjection(
authorizationValidation(async (request, response) => {
// request.roomId and request.playerId is undefined
});
);
But, when I do:
type Handler = typeof handler;
The Handler request parameter is perfectly typed.
Playground with working example
Is this a problem with Typescript or Next.js? Am I doing something wrong? Should I be using experimental decorators instead?