typescriptecmascript-6restful-architecturesoftware-design

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.


Solution

  • 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.