class-transformer
class-transformer copied to clipboard
question: how to create a top level extendable union type
I was trying to...
Create a top level union class which could be extendable by other classes for a user. Normally with just TypeScript alone it would look like this:
export type NonMemberDefinition = {
type: 'NON_MEMBER';
};
export type MemberDefinition = {
type: 'MEMBER';
points: number;
};
export type UserMembershipDefinition =
| NonMemberDefinition
| MemberDefinition;
export type AddMembershipPointsRequest = {
id: string;
} & UserMembershipDefinition;
export type AddMembershipPointsResponse = {
id: string;
} & UserMembershipDefinition;
So I've tried to do a something similar with class-transformer
with two approaches:
Approach 1
import { Exclude, Expose } from 'class-transformer';
import { IsEnum, IsNotEmpty, IsNumber } from 'class-validator';
import { MembershipType } from '../enums';
@Exclude()
export class NonMemberDefinition {
@Expose()
@IsEnum(MembershipType)
@IsNotEmpty()
public type: MembershipType = MembershipType.NON_MEMBER;
}
@Exclude()
export class MemberDefinition {
@Expose()
@IsEnum(MembershipType)
@IsNotEmpty()
public type: MembershipType = MembershipType.MEMBER;
@Expose()
@IsNumber()
@IsNotEmpty()
public points: number;
}
export type UserMembershipDefinition = NonMemberDefinition | MemberDefinition;
@Exclude()
export class AddMembershipPointsRequest extends UserMembershipDefinition {
@Expose()
@IsString()
@IsNotEmpty()
public id!: string;
}
@Exclude()
export class AddMembershipPointsResponse extends UserMembershipDefinition {
@Expose()
@IsString()
@IsNotEmpty()
public id!: string;
}
Approach 2
import { Exclude, Expose } from 'class-transformer';
import { IsEnum, IsNotEmpty, IsNumber, ValidateIf } from 'class-validator';
import { MembershipType } from '../enums';
@Exclude()
export class UserMembershipDefinition {
@Expose()
@IsEnum(MembershipType)
@IsNotEmpty()
public type!: MembershipType;
@Expose()
@ValidateIf((o) => o.type === MembershipType.MEMBER)
@IsNumber()
@IsNotEmpty()
public points: number;
}
@Exclude()
export class AddMembershipPointsRequest extends UserMembershipDefinition {
@Expose()
@IsString()
@IsNotEmpty()
public id!: string;
}
@Exclude()
export class AddMembershipPointsResponse extends UserMembershipDefinition {
@Expose()
@IsString()
@IsNotEmpty()
public id!: string;
}
The problem:
The problem with 1st approach is that it doesn't work because extending AddMembershipPointsRequest
sees UserMembershipDefinition
as a type (which is obvious) rather than a class but this would be a closes one to the pure TypeScript one.
The second approach works "well". the main issue lays in following part:
@Expose()
@ValidateIf((o) => o.type === MembershipType.MEMBER)
@IsNumber()
@IsNotEmpty()
public points: number;
In a controller I have a following piece of logic:
...
if (request.type === MembershipType.NON_MEMBER) {
return {
id,
type: MembershipType.NON_MEMBER,
};
}
return {
id,
points: previousPoints + request.points,
type: MembershipType.NON_MEMBER,
};
and issue here is that if I leave points
definition as it's it throws me an error in if case that points
is required in response (while it's not because NON_MEMBER doesn't require that there) but if I make it optional then in different place it's used it throws me an error that points are optional while they're not because points are defined in case of MEMBER.
Do you guys have any idea how it can be solved by using class-transform?