Class-transformer: feature: allow casting of primitive values during transformation

Created on 14 Jan 2021  路  5Comments  路  Source: typestack/class-transformer

Description

By design, class-transformer doesn't alter the value of properties when transforming an object into a class, but simply copies them over. However, there is an option (enableImplicitConversion) to change this behavior, when enabled the lib always attempts to cast the handled property value to the target property type. There are scenarios when some of the transformed properties should be casted in a non-standard way. The most common type of this is the casting stringified "true" and "false" to boolean. Currently, the library will cast both values to true, because Boolean("<any-non-zero-lenght-string>") will be always true.

There have been various discussions requesting to mark properties to be casted in a special way.

Proposed solution

There is two main way to approach this: adding decorators for each supported primitive type or enabling the feature at the transformation level via a new global transform option. Both have their pros and cons:

  • adding decorators makes it easier to customize the feature to the users need as they can choose to only transform specific properties with this new decorator
  • adding it as a global feature flag makes it easier to apply it at scale if someone needs this as the default behavior

Generally, I believe the decorator approach to be better and I propose the following decorators:

  • @CastToBoolean - transforms boolean, "true", "false", 0 and 1 to the appropriate boolean values
  • @CastToFloat - transforms a stringified number or number to the number representation, but not null or undefined
  • @CastToInteger - transforms a stringified number or number to an integer representation of the number, the number is rounded, but not null or undefined
  • @CastToString - transforms any received values to the string representation but not null or undefined
  • @CastToUndefined - transforms "undefined" to undefined
  • @CastToNull - transforms "null" to undefined

A single property can have multiple of these decorators applied.

Every one of these decorators accepts a single option:

@CastToBoolean({ force: boolean })

When set to true the decorator will always attempt to transform the received value (except when it's null or undefined). However, when it is false (the default value) it will only attempt to transform values when it makes sense. For example:

  • for booleans it will only transform "true", "false", 0, 1 and skip the transformation if any other value is received
  • for numbers it will only transform if the value can be parsed as a float number (aka: "not-number" will be skipped)

The transformation must take place before the implicit conversion of the value (if enabled). When enableImplicitConversion is enabled both transformations will run on the given property.

Please do not comment non-related stuff like +1 or waiting for this. If you want to express interest you can like the issue.

feature

Most helpful comment

This is how I got round the issue while managing to keep the boolean typing.

import { Transform } from 'class-transformer';

const ToBoolean = () => {
  const toPlain = Transform(
    ({ value }) => {
      return value;
    },
    {
      toPlainOnly: true,
    }
  );
  const toClass = (target: any, key: string) => {
    return Transform(
      ({ obj }) => {
        return valueToBoolean(obj[key]);
      },
      {
        toClassOnly: true,
      }
    )(target, key);
  };
  return function (target: any, key: string) {
    toPlain(target, key);
    toClass(target, key);
  };
};

const valueToBoolean = (value: any) => {
  if (value === null || value === undefined) {
    return undefined;
  }
  if (typeof value === 'boolean') {
    return value;
  }
  if (['true', 'on', 'yes', '1'].includes(value.toLowerCase())) {
    return true;
  }
  if (['false', 'off', 'no', '0'].includes(value.toLowerCase())) {
    return false;
  }
  return undefined;
};

export { ToBoolean };
export class SomeClass {
  @ToBoolean()
  isSomething : boolean;
}

All 5 comments

Did this get worked on? I'd like to help if this is still pending.

Not yet, feel free to pick it up! Do you have any change request for the proposal or do you want to take a crack at it in it's current form?

PS: If you made progress and I seem to be unavailable for more than a few days, feel free to ping me on Twitter. (I have 100+ notifications on Github, so some of them gets lost sometimes)

This is how I got round the issue while managing to keep the boolean typing.

import { Transform } from 'class-transformer';

const ToBoolean = () => {
  const toPlain = Transform(
    ({ value }) => {
      return value;
    },
    {
      toPlainOnly: true,
    }
  );
  const toClass = (target: any, key: string) => {
    return Transform(
      ({ obj }) => {
        return valueToBoolean(obj[key]);
      },
      {
        toClassOnly: true,
      }
    )(target, key);
  };
  return function (target: any, key: string) {
    toPlain(target, key);
    toClass(target, key);
  };
};

const valueToBoolean = (value: any) => {
  if (value === null || value === undefined) {
    return undefined;
  }
  if (typeof value === 'boolean') {
    return value;
  }
  if (['true', 'on', 'yes', '1'].includes(value.toLowerCase())) {
    return true;
  }
  if (['false', 'off', 'no', '0'].includes(value.toLowerCase())) {
    return false;
  }
  return undefined;
};

export { ToBoolean };
export class SomeClass {
  @ToBoolean()
  isSomething : boolean;
}

Any update on this?

Yeah, all great and needed to be done while a go.

When will this be released?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ERPedersen picture ERPedersen  路  4Comments

prbaron picture prbaron  路  3Comments

AckerApple picture AckerApple  路  5Comments

ceopaludetto picture ceopaludetto  路  3Comments

taemini picture taemini  路  5Comments