Property mapper - a TypeScript library

A solution to map structures in TypeScript Node projects

Posted on October 28, 2022 · 8 mins read

Property mapper for TypeScript

A use-case that annoyed me a lot: an object retrieved from an API can contain dates. We know that dates transferred between two systems often use an agreed syntax to make the communication easier. Most of the time, timestamps or iso formats are used.

In Angular, using HttpClient, it’s your responsibility to map this date representation into a Date object.

The most used solution is mapping the date in the observable returned by your API call:

public find(id: string): Foo {
    return this.httpClient.get<Foo>('/foo/' + id).pipe(
        map(foo => ({...foo,
            creationDate: new Date(foo.createdDate),
            modificationDate: new Date(foo.modificationDate)
        }))
    )
}

For a small object, this results in 3-5 lines added. For a most complicated object with nested dates, this could increase a lot the number of line and make your code less readable.

Another solution would be to create a regex that match the iso format. Then use this regex in an HttpInterceptor to transform the values that match the regex into dates.

This solution can be found here: StackOverflow

The issue with this approach, is that a user could change its nickname or any other personal data into an iso string. Your interceptor will then transform this date into a Date and your application may behave wrongly.

Solution three is a new solution that resolve both issues. It adds very few line of code and only targets specified fields.

Using a Decorator

@DateMapping('creationDate', 'modificationDate')
public find(id: string): Foo {
    return this.httpClient.get<Foo>('/foo/' + id)
}

So, what’s happening here? We create a decorator named DateMapping and we give as argument the keys that need to be mapped into Date.

Decorator implementation

export function DateMapping(...fields: string[]) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    // Save the original method implementation
    const oldMethod = descriptor.value;
    // Replace the method by a custom one
    descriptor.value = function (...args: any[]) {
      // Execute the original method
        const originalResponse = oldMethod(...args);
      // Handle asynchronous methods with Observables
      if (originalResponse instanceof Observable) {
        return originalResponse.pipe(
          map(response => mapToDates(response, fields))
        );
      }
      // Handle synchronous methods
      return mapToDates(originalResponse, fields);
    };
  }
}

function mapToDates(originalResponse: {[k: string]: any}, fields: string[]): {[k: string]: any} {
  return Object.entries<any>(originalResponse)
    .map<[key: string, value: any]>(([key, value]) => fields.includes(key) ? [key, new Date(<string|number> value)] : [key, value])
    .reduce<{[k: string]: any}>((acc, [key, value]) => ({...acc, [key]: value}), {});
}

The first method defines a Decorator. This Decorator will replace the original implementation of the targeted method by another implementation. In our case, we take care of executing the original implementation before handling the date mapping. Since our plans are to use this Decorator over http calls, we take extra precautions to also handle Observables.

The second method take care of mapping the given fields into dates.

  • The originalResponse is split into an array of key value;
  • Each key value couple is then mapped into a key Date if the key is included in the fields to be mapped;
  • Then the array of key value is reduced into an object to return the same type of the original response.

Can we go further?

Of course! We could map a subfield into a Date : @DateMapping('metadate.creationDate'). This could be tacked by using recursion

And why sticking to a Date? You could want to map any field to a class you created yourself @Mapping({ target: 'foo', method: (v) => new Foo(v) }).

A third extension could be to transform the type returned by mapping a source to a target @Mapping({ target: 'foo', source: 'bar' }).

With these 3 extensions, we have all the ingredients to build a mapping library.

Building a Mapper for TypeScript projects

Those of you who work with Java may know MapStruct. This library allows the developer to easily create mappings between objects. Most of the time, this library is used to map DTOs to Entities or any other model.

The MapStruct authors provides a powerful tool to help Java developers. Unfortunately, no tool like MapStruct currently exists for TypeScript (yet!).

As stated before, MapStruct is used to map stuffs into DTOs and Entities. Great! But this is backend stuffs, why would we need to map things in Angular?

First, for some projects, you could directly call an external API that provides objects in a structure you don’t like. Meaning that a mapping is required. For example, I had to do a frontend Mapping for this personal project: StepnTrader.

Second, this mapping could be used in a Node.JS server that need to do this DTO/Entity mapping.

That’s what you will find in this npm package: @woodchopper/property-mapper

This package is under MIT licence, don’t hesitate to try it!

Happy mapping!