How to properly extend the generic interface with a new generic parametr using decration merging in Typescript?

31 views Asked by At

I have a basic express app. Providing token based auth. Dealing with zod to create a schemas to validate the incoming data.

Saying I have two schemas:

  • createuserSchema {firstname, lastname, email, pass, passConfirm}
  • loginUserSchema {email, pass}

Zod allows us to infer types based on our schemas like: type SchemaBasedType = z.infer<typeof schmea> Those i have two types: CreateUserRequest and LoginUserRequest, based on my schemas.

First of all creating the validation middleware like this:

export const validateRequest =
    <T extends ZodTypeAny>(schema: T): RequestHandler =>
    async (req, res, next) => {
        try {
            const userRequestData: Record<string, unknown> = req.body;

            const validationResult = (await schema.spa(userRequestData)) as z.infer<T>;

            if (!validationResult.success) {
                throw new BadRequest(fromZodError(validationResult.error).toString());
            }

            req.payload = validationResult.data;
            next();
        } catch (error: unknown) {
            next(error);
        }
    }; 

As it mentioned above, this middleware acceps a schema argument, typed according to the zod docs. In my opinion, it's a good desicion to extend the request object with the "payload" property, where i can put my valid data.

Then the problems begin. TS doesn't know what the payload actually is. That's where the declaration merging is comming. Firstly, it's tempting to try something like this:

declare global {
    namespace Express {
        export interface Request {
            payload?: any;
            
        }
    }
}

But seems it's not a good idea, cause we know exactly what is our payload signature. Then I tried a union type, based on zod types:

payload?: CreateUserRequest | LoginUserRequest;

With this approach I caught a mistake, that some of the fields of more narrow type doesn't exist in other type;

Then i tried to use a generic,

declare global {
    namespace Express {
        export interface Request<T> {
            payload?: T;
            
        }
    }
}

and it seems like a solution, but the Request interface already has 5 generic arguments:

interface Request<
    P = ParamsDictionary,
    ResBody = any,
    ReqBody = any,
    ReqQuery = ParsedQs,
    LocalsObj extends Record<string, any> = Record<string, any>

and I can't even imagine how it suppose to merge, meaning my generic argument will be the first, or the last? Anyway it's seems as a wrong way, cause after all I don't see the extended interface via the IDE hints. Somewhere on stack I met this approach:

declare global {
    namespace Express {
        export interface Request<
            Payload = any,
            P = ParamsDictionary,
            ResBody = any,
            ReqBody = any,
            ReqQuery = ParsedQs,
            LocalsObj extends Record<string, any> = Record<string, any>
        > {
            payload?: Payload;
        }
    }
}

And it even give me a hint after mouse guidence, but i'm not sure that any is a good type cause we already have types, infered by zod. If not specify Payload = any, I don't receive a hint in a type.

I have no idead and stack with it, cause i'm not an expert in ts and backend architecture;

Finally, i'd like to get something like this:

authRouter.post("/register", validateRequest(createUserSchema), AuthController.register); where the compiler knows, that the payload signature is equal to CreateUserRequest authRouter.post("/login", validateRequest(loginUserSchema), AuthController.login); where the compiler knows, that the payload signature is equal to LoginUserRequest

Where should I properly specify my expected types and how to deal with them?

0

There are 0 answers