DTO implementation in Typescript

Bez kategorii

I want to write in this article about best in my opinion DTO implementation in Javascript language with Typescript subset. I will talk about two ways: declarative and atomizing. In two cases we will have a complaint situation where receipt will be gained by L1 support service and passed finally to expert voter (as aggregate root). So let's go!! :)

Atomizing approach

Architecture visualisation:

In atomizing approach we'll do more in static DTO mapper. So lets describe what will happen step by step. First of all we need to define our DTO class. Let it to be a ReceiptDetails and PotentiallyDamagedProduct. To achieve atomization we'll use constructor property assigment from Typescript.

// App/Dto/ReceiptDetailsDTO.js
export default class ReceiptDetailsDTO {
   constructor(
      private readonly _ownerId: string,
      private readonly _generationDate: Date,
      private readonly _totalPrice: number,
      
   ) {}
}

// App/Dto/PotentiallyDamagedProductDTO.js
export default class PotentiallyDamagedProductDTO {
    constructor(
      private readonly _condition: string,
      private readonly _wariantyDurationInMonths: number
    ) {}
}

As you see i used private keyword with readonly. Also specified dash as prefix for properties. It's because ideal DTO's contains only getters to access data . In old javascript, dash before property symbolised conventionally called private property. This allows to define getters names originally as well.

Next step is retrieve data service traditionally. After this we need to prepare them via parser for data structure. In our situation this will be JSON simply.

import { buildEndpoint, buildHttpClient, __complaintAPIBaseURL } from 'helpers';
import DTOAutoMapper from '../../domain/mapper/DTOAutoMapper';

class L1SupportService {
   construct() {/* Configure your client */this.httpClient = buildHttpClient();}   

   public async fetchComplaintPotentiallyDamagedProductDetails(complaintId: UuidInterface): Promise<PotentiallyDamagedProductDTO> {
      const response = await this.httpClient.getFromEndpoint(
           buildEndpoint(__complaintAPIBaseURL, 'comlaint', complaintId.toString());

      const parsedData = JSON.parse(response.data);
      return DTOAutoMapper.fromObject<PotentiallyDamagedProductDTO>(PotentiallyDamagedProductDTO, parsedData);
      );
   }
}

Then we got anonymous javascript object and we need to pass it to our DTO static mapper. This one will auto generate getters in dto and inject values in right order to constructor. So let's check the DTO static mapper class.

export default class DTOAutoMapper {
    public static fromObject<DTOType>(targetDTOClass: new (...dtoProps: any[]) => DTOType, foreignObject: Record<string, any>): DTOType
    {
        for (const propName of Object.getOwnPropertyNames(targetDTOClass)) {
            Object.defineProperty(targetDTOClass.prototype, propName.replace('_', ''), {
                get:function() {
                    return this[propName];
                },
                writable:false,
                configurable:false,
                enumerable:true
            });
        }
        const targetDTOGetters: any[] = Object.entries(Object.getOwnPropertyDescriptors(targetDTOClass))
            .filter(([, descriptor]) => typeof descriptor.get === 'function')
            .map(([key]) => foreignObject['_' + key]);
        return new targetDTOClass(...targetDTOGetters);
    }
}

As you see in output of fromObject method we will obtain ready DTO object. So lets pass our DTO to application throught adapter to application. We can now transform DTO into Domain Object of being kind that's will be represent bussines object obviously. Our another purpose will be a convert DTO to DO using already defined DO and use setter in them to transform all data which is compatible between classes (in getters and setters). I'll use a build-in descriptor setter feature but functions can be used also. Just look to static domain object mapper.

export default class DomainObjectAutoMapper {
    public static fromDTO<DTOType, DOType>(targetDTO: DTOType, targetDomainObject: DOType): void
    {
        const dtoGetters = Object.entries(targetDTO)
            .filter(([, descriptor]) => typeof descriptor.get === 'function')
            .map(([key]) => key);

        const doSetters = Object.entries(targetDomainObject)
            .filter(([, descriptor]) => typeof descriptor.set === 'function')
            .map(([key]) => key);

        for (const doSetter of doSetters) {
            if (dtoGetters.includes(doSetter)) {
                targetDomainObject[doSetter] = targetDTO[doSetter];
            }
        }
    }
}

Okay, we got a DO, in this case entity because our PotentiallyDamagedProduct have unique ID. So in final step we can do something like use ComplaintVoter (aggregate) with ComplaintPolicy to determine decision, that is worth to accept or reject complaint. Is very simplified but that was intentionally.

Declarative Try

I will focus on this part to compare differences, i think it will be enought and clear. The main is way to declare DTO's. Therefore we'll define getters manually but with real private properties. So below you can se our potentially damaged product dto.

// App/Dto/PotentiallyDamagedProductDTO.js
export default class PotentiallyDamagedProductDTO {
     private readonly #condition: string;
     private readonly #wariantyDurationInMonths: number;

     get condition(): string {
        return this.#confition;
     }

     get wariantyDurationInMonths(): number
     {
        return this.#wariantyDurationInMonths;
     }
}

As you see this time hermetization will works as expected. It affect to reduce DTO static mapper responsibility, because now it doesn't have to generate a getters anymore.

export default class DTOAutoMapper {
    public static fromObject<DTOType>(targetDTOClass: new (...dtoProps: any[]) => DTOType, foreignObject: Record<string, any>): DTOType
    {
        const targetDTOGetters: any[] = Object.entries(Object.getOwnPropertyDescriptors(targetDTOClass))
            .filter(([, descriptor]) => typeof descriptor.get === 'function')
            .map(([key]) => foreignObject['_' + key]);
        return new targetDTOClass(...targetDTOGetters);
    }
}

Summary

Atomized Pros:

  • More automated
  • Less code of DTO's, even minimal

Atomized Cons:

  • Properties defined with private keyword is not really private

Declarative Pros:

  • Properties are hermitizatizated in right way
  • Less responsibility rely on mapper, so faster execution and more stable code.

Declarative Cons:

  • Requires more time to define DTO if we do not have a webstorm i.e.
  • More code in DTO.

Thanks for reading and have a good day! Your feedback below linkedin comment will be appreciated about article. I turned off a comments feature in blog for security reasons until beign fixed.