Description
I've spent some time figuring this problem out, and since the repo isn't being actively maintained right now I figure the best I can do to give other's access to my findings is to put them in an issue.
In TransformOperationExecutor.ts
(.js
if you are digging through the compiled code from node_modules
like I was), the library tries to figure out how to construct it's target type, and under certain conditions will reach line 94:
newValue = new (targetType as any)();
(or line 86 in the JS version):
newValue = new targetType();
This just blindly calls the target type's constructor with no arguments, which works for classes that don't have real constructors that expect arguments, but doesn't work for classes that have constructors that validate their arguments at runtime.
I think this is probably what the code should be doing in this case, there's no way for it to figure out if the constructor expects arguments, or what it does with them, and it works for many use cases.
But there are plenty of cases where it won't work, and those can be handled in a way that will at least give more useful errors.
The Firebase/Firestore SDK's are switching to using their own Timestamp
class instead of the native Date
class for storing time values, and the constructor to that class expects two numbers, and throws a runtime exception if they are not present and valid.
Ideally, TransformOperationExecutor
would wrap the call to the constructor in a try-catch block, so it could print a helpful error message like:
"the constructor for the target class threw an error, this class may not be compatible with class-transformer..."
then re-throw the caught error. This would make it much more obvious what is going on, and wouldn't change the runtime behavior at all, a strict improvement over the current functionality.
I would gladly submit a PR for this, but PR's aren't getting reviewed right now so I'm not going to invest my time in that.
For anyone else that runs into this compatibility issue, or something similar, I have found a fairly respectable workaround.
export class User {
@IsString()
@IsEmail()
email!: string;
@Transform((value: any) => {
const seconds: number = value._seconds;
const nanoseconds: number = value._nanoseconds;
return new Timestamp(seconds, nanoseconds);
})
@Type(() => Object)
created_time!: Timestamp;
}
This example is for the specific types I was working with, but the pattern can be used in many other cases.
I use @Type(() => Object)
to bypass the call to the Timestamp constructor, class-transformer will just give me a plain object copy of the source data for that key,
Then I use @Transform
to hook into the transformation lifecycle, and craft a valid call to the constructor out of the Object.
I've seen a few open issues in this repo that could potentially be helped by this pattern, it may be a good idea to add a note about it to the readme, or add a new decorator that accomplishes the same thing, giving people a simpler way to write their own custom transformers.
I appreciate how useful this library is, combined with class-validator it lets me easily add runtime-validation of dynamic data without having to write a ton of custom code, it's just unfortunate that it isn't being maintained well right now.