DTO Design in TypeScript/Angular2

I use class-transformer for DTOs design. It does all dirty job and provides @Expose(), @Exclude(), @Transform(), @Type() as well as several other helpful property annotations. Just read docs.

Here is an example:

  • Basic DTO handles serialization and deserialization automatically.
  • @Transform converts SQL date to/from string. You may use your own custom transformers.
import {
  classToPlain,
  plainToClass,
  Transform,
  TransformationType,
  TransformFnParams
} from 'class-transformer';
import { DateTime } from 'luxon';

/**
 * Base DTO class.
 */
export class Dto {
  constructor(data?: Partial<Dto>) {
    if (data) {
      Object.assign(this, data);
    }
  }

  /**
   * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#tojson_behavior
   */
  toJSON(): Record<string, any> {
    return classToPlain(this);
  }

  static fromJSON<T extends typeof Dto>(this: T, json: string): InstanceType<T> {
    return this.fromPlain(JSON.parse(json));
  }

  /**
   * @see https://github.com/Microsoft/TypeScript/issues/5863#issuecomment-528305043
   */
  static fromPlain<T extends typeof Dto>(this: T, plain: Object): InstanceType<T> {
    return plainToClass(this, plain) as InstanceType<T>;
  }
}

/**
 * SQL date transformer for JSON serialization.
 */
export function sqlDateTransformer({type, value}: TransformFnParams): Date | string {
  if (!value) {
    return value;
  }

  switch (type) {
    case TransformationType.PLAIN_TO_CLASS:
      return DateTime.fromSQL(value as string).toJSDate();

    case TransformationType.CLASS_TO_PLAIN:
      return DateTime.fromJSDate(value as Date).toFormat('yyyy-MM-dd HH:mm:ss');

    default:
      return value;
  }
}

/**
 * Example DTO.
 */
export class SomethingDto extends Dto {
  id?: string;
  name?: string;

  /**
   * Date is serialized into SQL format.
   */
  @Transform(sqlDateTransformer)
  date?: Date;

  constructor(data?: Partial<SomethingDto>) {
    super(data);
  }
}

// Create new DTO
const somethingDto = new SomethingDto({
  id: '1a8b5b9a-4681-4868-bde5-95f023ba1706',
  name: 'It is a thing',
  date: new Date()
});

// Convert to JSON
const jsonString = JSON.stringify(somethingDto);
console.log('JSON string:', jsonString);

// Parse from JSON
const parsed = SomethingDto.fromJSON(jsonString);
console.log('Parsed:', parsed);

You can use a property decorator for this:

const DOT_INCLUDES = {};

function DtoInclude(proto, name) {
    const key = proto.constructor.name;
    if (DOT_INCLUDES[key]) {
        DOT_INCLUDES[key].push(name);
    } else {
        DOT_INCLUDES[key] = [name];
    }
}

class A {
    @DtoInclude
    public x: number;
    public y: number;

    @DtoInclude
    private str: string;

    constructor(x: number, y: number, str: string) {
        this.x = x;
        this.y = y;
        this.str = str;
    }

    toDTO(): any {
        const includes: string[] = DOT_INCLUDES[(this.constructor as any).name];
        const dto = {};

        for (let key in this) {
            if (includes.indexOf(key) >= 0) {
                dto[key] = this[key];
            }
        }

        return dto;
    }
}

let a = new A(1, 2, "string");
console.log(a.toDTO()); // Object {x: 1, str: "string"}

(code in playground)

You can use the reflect-metadata that is used in their examples if you want, I implemented it with the DOT_INCLUDES registry so that it will work well within the playground without the need for extra dependencies.


Edit

As @Bergi commented, you can iterate over the includes instead of this:

toDTO(): any {
    const includes: string[] = DOT_INCLUDES[(this.constructor as any).name];
    const dto = {};

    for (let ket of includes) {
        dto[key] = this[key];
    }

    return dto;
}

Which is indeed more efficient and makes more sense.