Reputation: 1065
I want to create own class controller for handling express requests.
What I have:
Controller:
import { Request, Response } from 'express';
import { transvalidate } from './schema/transform-validate';
import { ClassConstructor } from 'class-transformer';
export abstract class Controller<QuerySchema extends object> {
querySchema?: ClassConstructor<QuerySchema>;
query?: QuerySchema;
constructor(
protected readonly req: Request,
protected readonly res: Response,
) {
if (this.querySchema) {
this.query = transvalidate(this.querySchema, this.req.query);
}
}
getQuery(): QuerySchema {
if (!this.query) {
throw new Error(
`Can't get query. Probably the querySchema is not provided.`,
);
}
return this.query;
}
abstract handle(): Promise<unknown>;
}
GetCatsController:
import { Controller } from './controller';
import { GetCatsQuerySchema } from './schema/query.schema';
export class CatsController extends Controller<GetCatsQuerySchema> {
querySchema = GetCatsQuerySchema;
handle(): Promise<unknown> {
// here this.getQuery() returns transformed to GetCatsQuerySchema and validated req.query object
return Promise.resolve(undefined);
}
}
GetCatsQuerySchema
:
import { IsInt, IsOptional } from 'class-validator';
import { Type } from 'class-transformer';
export class GetCatsQuerySchema {
@Type(() => Number)
@IsInt()
@IsOptional()
age?: string;
}
transform-validate
function:
import 'reflect-metadata';
import { ClassConstructor, plainToClass } from 'class-transformer';
import { validateSync, ValidationError } from 'class-validator';
export const transvalidate = <T extends object>(
targetClass: ClassConstructor<T>,
rawData: unknown,
) => {
const transformedValue = plainToClass(targetClass, rawData);
const errors = validateSync(transformedValue);
if (errors.length) {
const errorMessages = getAllErrorMessages(errors);
throw new Error(errorMessages[0]);
}
return transformedValue;
};
const getAllErrorMessages = (errors: ValidationError[]) => {
const errorMessages: string[] = [];
errors.map((error) => {
if (error.constraints) {
errorMessages.push(...Object.values(error.constraints));
}
});
return errorMessages;
};
But I don't like that each class should provide QuerySchema
when extends Controller:
class CatsController extends Controller<GetCatsQuerySchema> {
Instead of this I want to find a way to define QuerySchema in the class properties:
export class CatsController extends Controller {
querySchema = GetCatsQuerySchema;
Is it possible?
Upvotes: 0
Views: 1077
Reputation: 249636
If the class is generic you have to explicitly specify the type argument. And this is the approach I would definitely go for.
Now if the class is not generic, you don't have to specify the generic type parameter. The only issue is that you would need to instead of QuerySchema
find a way to say 'use whatever type is in the derived class'. Fortunately typescript supports polymorphic this
, which basically means whatever the current type is. We can use the polymorphic this
type to extract the actual type of querySchema
in the derived type:
type QuerySchema<T extends {querySchema?: ClassConstructor<object>}> =
InstanceType<Exclude<T['querySchema'], undefined>>
export abstract class Controller {
querySchema?: ClassConstructor<object>;
query?: QuerySchema<this>;
constructor(
protected readonly req: Request,
protected readonly res: Response,
) {
if (this.querySchema) {
this.query = null!;
}
}
getQuery(): QuerySchema<this> {
// ...
}
abstract handle(): Promise<unknown>;
}
export class CatsController extends Controller {
querySchema = GetCatsQuerySchema;
handle(): Promise<unknown> {
this.getQuery().age
this.getQuery().age1 // error
return Promise.resolve(undefined);
}
}
export class GetCatsQuerySchema {
age?: string;
}
This seems to work, but you will probably run into issues with assignability sooner or later due to the conditional types, so I would stick with the generic approach. So use at your own risk.
Upvotes: 1