-
Notifications
You must be signed in to change notification settings - Fork 520
Description
Description
plainToInstance
throws error on Decimal values, when they are already instanceof Decimal. This is relevant in situations, where the plainobject came from within the application and not over the network.
For plainobjects from JSON, this is not an issue, as Decimal values are always transported as a string.
Minimal code-snippet showcasing the problem
class FailingDto {
@Transform(({ value }) => {
console.log('transform', value);
return new Decimal(value); // instanceToPlain direction omitted for brevity
})
public value!: Decimal;
}
plainToInstance(FailingDto, { value: 1 })
// console.log is printed
// returns instance with Decimal(1), as expected
plainToInstance(FailingDto, { value: new Decimal(1) })
// console.log is not printed
// throws "[DecimalError] Invalid argument: undefined"
Same issue happens also without any Transform
, e.g. with following DTO:
class FailingDto {
@Expose()
public value!: Decimal;
}
Expected behavior
It should be possible to write a Transformer for Decimal
values, without needing the String
workaround below. (Or not having to write transformer at all would be best ofc.)
Note: new Decimal(new Decimal(1))
works just fine
Actual behavior
Throws [DecimalError] Invalid argument: undefined
error
The error originates at newValue = new (targetType as any)();
in TransformOperationExecutor.ts:160 where class-transformer attempts to create a new instance without any constructor arguments. The Decimal
constructor requires a value parameter and throws when called without arguments.
Current workarounds
Annotate the field with @Type(() => String)
below the Transform
.
But this is not ideal, as the field is not "String" it's "Decimal", so it's confusing for the reader.
Also it changes the value the @Transform
will get, which is usually not relevant (Decimal values are usually serialized as string anyway), but it can become a footgun in some edge-cases (e.g. when I want to write transformer, that accepts Decimal | string | [number, number]
).
class NoLongerFailingDto {
@Transform(({ value }) => {
console.log('transform', value);
return new Decimal(value); // instanceToPlain direction omitted for brevity
})
@Type(() => String) // Must be String, not Number, to preserve decimal precision
public value!: Decimal;
}
It looks more acceptable, when you hide the hack under a custom decorator, but still has the issue of unwanted string cast:
// nullable and optional variants omitted for brevity
function CustomDecorator(): PropertyDecorator {
// instanceToPlain direction omitted for brevity
const transformDecorator = Transform(({ value }) => new Decimal(value));
const typeDecorator = Type(() => String);
return (target, propertyKey) => {
typeDecorator(target, propertyKey);
transformDecorator(target, propertyKey);
};
}
class NoLongerFailingDto {
@CustomDecorator()
public value!: Decimal;
}
Dependency versions
class-transformer: 0.5.1
typescript: 5.6.3
decimal.js: 10.4.3
Related issues
- plainToClass Transform ignored when nested is a newable (decimal.js problem and solution) #1270
- work for me too #1821
- fix: plainToClassFromExist is not work for type Decimal #1531
- fix: Transform fails if the constructor expects an argument #1385
- plainToClass: Transform ignored when nested is a newable #1138