Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

DTO Design in TypeScript/Angular2

I'm currently developing an Angular 2 application. While developing I started to use TypeScript classes to create objects from JSON I receive through HTTP or when creating a new object in a form.

The class may for example look like this.

export class Product {
    public id: number;
    public name: string;
    public description: string;
    public price: number;
    private _imageId: number;
    private _imageUrl: string;

    constructor(obj: Object = {}) {
        Object.assign(this, obj);
    }

    get imageId(): number {
        return this._imageId;
    }
    set imageId(id: number) {
        this._imageId = id;
        this._imageUrl = `//www.example.org/images/${id}`;
    }

    get imageUrl(): string {
        return this._imageUrl;
    }

    public getDTO() {
        return {
            name: this.name,
            description: this.description,
            imageId: this.imageId,
            price: this.price
        }
    }
}

So far this solution shown above works great. But now let's assume that there are a lot more properties in the object and I want a clean DTO (for example without the private properties) for sending this Object by POST to my server. How could a more generic getDTO() function look like? I would like to avoid having a long list of property assignment. I was thinking about using decorators for the properties. But I don't really know how to use them to filter the properties for the DTO.

like image 435
DaSch Avatar asked Sep 01 '16 13:09

DaSch


2 Answers

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);
like image 184
Viacheslav Dobromyslov Avatar answered Nov 10 '22 03:11

Viacheslav Dobromyslov


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.

like image 7
Nitzan Tomer Avatar answered Nov 10 '22 01:11

Nitzan Tomer