Mapping Objects in Angular 2+

TLDR: skip to the end if you just want to see my solution

Automapper

Back in the days of madness and C# programs, we used a library called Automapper.  We loved it and hated it.  It was used correctly and incorrectly.  Folks new to the team grasped their hair as if to pull it out (bald men were relegated to imaginary hair) and screamed.

Truthfully, Automapper is a fine library when used correctly.  Its purpose is to take one object and convert it into another with as few lines of codes as possible.  For example, you might receive an object from an api or a middle tier/business layer.  It has some properties useful to the client, and some that are not.  The corresponding client object(s) might have additional properties useful to the client.

It gets really boring to write code to convert one object to another, and for this reason Automapper exists.

automapper-ts and other approaches to solve mapping

In my current Angular 2+ project, I have a need to map objects coming from outside the client to client models, and client models to client view models, and the reverse.  I tried things.  Which things?

Before we talk about that, let me say that searching for mapping solutions most of the time brought up information on array mapping.  That’s a different thing.  The map function there applies a specified function to each element of an array– not what I am talking about here.

I remembered the nefarious Automapper from C# days and thought perhaps there might be such a thing for TypeScript.  There is:  automapper-ts.  The solution to all my problems!  Inside my head I was doing the Snoopy dance.

And then I tried to use it.

It’s not written in a modular way, and despite my following instructions to the letter written by people who had also had issues with importing/requiring/using automapper-ts, I could not get it to work.  The author of the automapper-ts library knows about this issue and has attempted to provide solutions for it, but found that he could not support both a modular approach and whatever you call the previous approach, so has not implemented a solution.

After a day of fighting with what should have been simple, I uninstalled automapper-js and continued to think.  I should have been able to use instanceof() to see if an object was an instance of a class, and use an if/else statement to map it.

public doMap(sourceObject: any): any {
   if (sourceObject instanceof(UserDto)) {
      // convert UserDto to User
   } else {

      if (sourceObject instanceof(User)) {
         // convert User to UserDto
      }
   ... etc
   }

TypeScript did not like this, and my code would not compile for reasons I failed to specifically record.  Something like ‘type is used as a value’ even though there were examples around the web showing this exact usage.

I developed the notion that it might be because my ‘type’ was really an interface.  I have learned that interfaces are best used instead of classes if there are no operations to be implemented– i.e. it’s a data class with properties only, and none of them need to be private.  This is really just a complex datatype, right?  When classes are used, the transpiled JavaScript converts the class to a function, which is unnecessary code bloat.  Conversely, interfaces do not get transpiled, and do not appear in the final code.  However, they do provide what we want at compile time– type checking.  Here is an excellent blog post on classes vs. interfaces.

For these reasons, I try to limit my use of classes to services and components.

As it turned out, the idea that it was the interface vs. class issue causing my if/else tree to fail was not the case.  But I was tired of trying things to make instanceof() or typeof() to work.

My eventual solution?

A Converter Service

Services in Angular are nice because they are injectable, and as such, the service class can be treated like a singleton.

My mapper– which I chose to call a Converter so as not to be confused with map functions on arrays, is a class called Converter with an @Injectable() attribute.  I added it to the list of providers in the AppModule, and can inject it anywhere.

It looks like this:

@Injectable()
export class Converter {

   public toUser(userDto: UserDto): User {
      const user: User = {} as User;

      user.id             = userDto.id;
      user.thirdPartyId   = userDto.thirdPartyId;
      user.thirdPartyName = userDto.thirdPartyName;
      user.plan           = null; // todo: get from Store

      return user;
   }

   public toUserDto(user: User): UserDto {
      const userDto = {} as UserDto;

      userDto.id               = user.id;
      userDto.thirdPartyId     = user.thirdPartyId;
      userDto.thirdPartyName   = user.thirdPartyName;
      userDto.planId           = user.plan.id;

      return userDto;
   }
}

 
In time, the Converter service will contain quite a few functions, and will include functions to convert models to view models, such as

toUserViewModel(user: User): UserViewModel.

This approach works well because code that uses the converter can be checked at compile time to be sure the correct types are being used.  Here is an example of an ngrx effect that uses the converter. (In the constructor of the UserEffectsService I have injected the Converter service as converter).  With this effect, if the call to the UserDataService returns an actual user dto (data transfer object) we know the user exists, otherwise we add the user to the data store.

export class UserEffectsService {
    constructor( private userDataService: UserDataService,
                 private converter: Converter,
                 private action$: Actions) {}

@Effect()
getUser$: Observable = this.action$
 .ofType(GET_USER)
 .map((action: GetUserAction) => action.payload)
 .mergeMap((user: User) =>

 this.userDataService.getUser(user)
 .switchMap((data: any) => {
 if (data.result && (isUserDto(data.result))) {

 const existingUser: User = this.converter.toUser(data);
 return [new GetUserSuccessAction(existingUser)];

 } else {
 return [new AddNewUserAction(user)];
 }
 })
 .catch(error => of(new GetUserFailureAction(error)))
 );

 

Problem Solved!

 

 

 

 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s