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.