Skip to content

fix: plainToInstance from Decimal to Decimal throws an error (decimal.js) #1829

@FilipSvozil

Description

@FilipSvozil

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    status: needs triageIssues which needs to be reproduced to be verified report.type: fixIssues describing a broken feature.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions