'Can't merge generic type in callbacks with Nextjs and TypeScript
I'm currently developing a project in NextJS and implementing callbacks as decorators to enhance Next API handler (join.ts). While developing it I was able to enhance the NextApiRequest interface successfully, combining props from each callback/decorator, in the end, providing the request with additional properties that I may access in the handler. However, while trying the same thing with NextApiResponse, I wasn't able to pass down the response that the handler will provide, only being able to type the first callback response, which ends up like this:
type RoomsRoomIdJoinResponse = RoomsRoomIdJoinErrors | RequestValidationErrors
My files:
import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next';
import { verify } from 'jsonwebtoken';
import {
ExpiredCredentialsError,
InvalidCredentialsError,
MissingCredentialsError,
UnexpectedError,
} from './pages/api/errors';
type AuthorizationValidationErrors =
| MissingCredentialsError
| InvalidCredentialsError
| ExpiredCredentialsError
| UnexpectedError;
type ApiHandler<Request, Response> = (
request: Request & { headers: { authorization: string } },
response: Response | NextApiResponse<AuthorizationValidationErrors>
) => ReturnType<NextApiHandler<Response>>;
type DecoratedApiHandler<Request, Response> = (
request: Request,
response: Response | NextApiResponse<AuthorizationValidationErrors>
) => ReturnType<NextApiHandler<Response>>;
const authorizationValidation = <
Request extends NextApiRequest,
Response extends NextApiResponse
>(
handler: ApiHandler<Request, Response>
) => {
const decoratedHandler: DecoratedApiHandler<Request, Response> = async (
request,
response
) => {
const {
headers: { authorization },
} = request;
let statusCode = 401;
let json: AuthorizationValidationErrors = new MissingCredentialsError(
'Missing credentials'
);
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 = 401;
json = new InvalidCredentialsError('Invalid credentials');
}
} catch ({ name: errorName }) {
if (errorName === 'JsonWebTokenError') {
statusCode = 401;
json = new InvalidCredentialsError('Invalid credentials');
} else if (errorName === 'TokenExpiredError') {
statusCode = 403;
json = new ExpiredCredentialsError('Expired credentials');
} else {
statusCode = 500;
json = new UnexpectedError(
'Unexpected error while processing authorization'
);
}
}
} else {
response.status(statusCode).json(json);
}
};
return decoratedHandler;
};
export default authorizationValidation;
import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next';
import { decode } from 'jsonwebtoken';
type ApiHandler<Request, Response> = (
request: Request & { roomId?: string; playerId?: string },
response: Response
) => ReturnType<NextApiHandler<Response>>;
type DecoratedApiHandler<Request, Response> = (
request: Request,
response: Response
) => ReturnType<NextApiHandler<Response>>;
const decodedJwtInjection = <
Request extends NextApiRequest,
Response extends NextApiResponse
>(
handler: ApiHandler<Request, Response>
) => {
const decoratedHandler: DecoratedApiHandler<Request, Response> = async (
request,
response
) => {
const {
headers: { authorization },
} = request;
if (authorization) {
const decodedJwt = decode(authorization);
if (
decodedJwt &&
typeof decodedJwt === 'object' &&
'roomId' in decodedJwt &&
'playerId' in decodedJwt
) {
request = {
...request,
roomId: decodedJwt.roomId,
playerId: decodedJwt.playerId,
};
}
}
await handler(request, response);
};
return decoratedHandler;
};
export default decodedJwtInjection;
import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next';
import { ajv } from '.';
import {
BadRequestError,
InvalidCredentialsError,
MethodNotAllowedError,
MissingCredentialsError,
UnexpectedError,
} from './pages/api/errors';
type RequestValidationErrors =
| MissingCredentialsError
| UnexpectedError
| MethodNotAllowedError
| BadRequestError
| UnexpectedError;
type ApiHandler<Request, Response> = (
request: Request,
response: Response | NextApiResponse<RequestValidationErrors>
) => ReturnType<NextApiHandler<Response>>;
type DecoratedApiHandler<Request, Response> = (
request: Request,
response: Response | NextApiResponse<RequestValidationErrors>
) => ReturnType<NextApiHandler<Response>>;
const requestValidation = <
Request extends NextApiRequest,
Response extends NextApiResponse
>(
handler: ApiHandler<Request, Response>,
schemaId: string
) => {
const decoratedHandler: DecoratedApiHandler<Request, Response> = async (
request,
response
) => {
let json: RequestValidationErrors = new UnexpectedError('Invalid schema');
let statusCode = 500;
const ajvInstance = ajv.getInstance();
const validate = ajvInstance.getSchema(schemaId);
if (validate) {
const valid = validate(request);
if (valid) {
await handler(request, response);
} else {
json = new BadRequestError('Bad request');
statusCode = 400;
const errors = validate.errors;
if (errors && errors.length) {
const error = errors[0];
if (error.instancePath === '/method') {
statusCode = 405;
json = new MethodNotAllowedError('Method not allowed');
}
if (error.instancePath === '/headers/authorization') {
statusCode = 401;
json = new InvalidCredentialsError('Invalid credentials');
}
if (
error.instancePath === '/headers' &&
error.params.missingProperty === 'authorization'
) {
statusCode = 401;
json = new MissingCredentialsError('Missing credentials');
}
if (error.message) {
json.message = `Request ${error.instancePath.slice(1)} ${
error.message
}`
.replace(/\s+/g, ' ')
.trim();
if (error.keyword === 'enum') {
json.message += `: ${error.params.allowedValues.join(', ')}`;
}
}
}
response.status(statusCode).json(json);
}
} else {
response.status(statusCode).json(json);
}
};
return decoratedHandler;
};
export default requestValidation;
import { verify } from 'jsonwebtoken';
import { NextApiRequest, NextApiResponse } from 'next';
import {
authorizationValidation,
connectionFactory,
decodedJwtInjection,
requestValidation,
} from '../../../../utils';
import {
ForbiddenAccessError,
InvalidCredentialsError,
NotFoundError,
UnexpectedError,
} from '../../../../utils/pages/api/errors';
import UnprocessableError from '../../../../utils/pages/api/errors/UnprocessableError';
type RoomsRoomIdJoinErrors = NotFoundError | ForbiddenAccessError | undefined;
type Handler = typeof handler;
export type RoomsRoomIdJoinResponse = Parameters<
Parameters<Handler>[1]['send']
>[0];
const handler = requestValidation<
NextApiRequest,
NextApiResponse<RoomsRoomIdJoinErrors>
>(
authorizationValidation(
decodedJwtInjection(async (request, response) => {
let statusCode;
let json;
const redis = connectionFactory().createRedisConnection(
process.env.REDIS_URL
);
const {
headers: { authorization },
} = request;
try {
const decodedPayload = verify(
authorization as string,
process.env.SECRET_KEY
);
if (
decodedPayload &&
typeof decodedPayload === 'object' &&
'roomId' in decodedPayload
) {
const { query } = request;
if (decodedPayload.roomId === query.roomId) {
const roomExists = await redis.exists(query.roomId as string);
if (roomExists) {
const votes = await redis.hget(decodedPayload.roomId, 'votes');
if (votes) {
const parsedVotes = JSON.parse(votes);
const isNewPlayer = !parsedVotes.some(
(player: object) => decodedPayload.playerId in player
);
if (isNewPlayer) {
const newVotes = JSON.stringify([
...parsedVotes,
{ [decodedPayload.playerId]: '' },
]);
await redis.hset(decodedPayload.roomId, 'votes', newVotes);
}
} else {
statusCode = 422;
json = new UnprocessableError('Malformed room structure');
}
statusCode = 204;
json = undefined;
} else {
statusCode = 404;
json = new NotFoundError('Room not found');
}
} else {
statusCode = 403;
json = new ForbiddenAccessError('Forbidden access to this room');
}
} else {
statusCode = 401;
json = new InvalidCredentialsError('Invalid credentials');
}
} catch {
statusCode = 500;
json = new UnexpectedError('Unexpected error while processing request');
}
response.status(statusCode).json(json);
})
),
'RequestSchema'
);
export default handler;
My goal is to achieve something like this:
type RoomsRoomIdJoinResponse = RoomsRoomIdJoinErrors | RequestValidationErrors | AuthorizationValidationErrors
Any idea how to solve this? :)
Edit #1 - Added code in question to the post as suggested by @juliomalves
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|
