Mapper en typescript
En distintas ocasiones necesitamos pasar desde una representación de un objeto del dominio a otro tipo de representación acorde con las necesidades de la situación o aplicación. Para hacer esta transformación se utiliza un mapper en typescript.
Un mapper no será más que una lógica por la cual trasladamos e incluso transformamos las propiedades de un objeto de entrada a otro de salida.
Usos
En nuestro caso, siendo una aplicación en angular, podemos tener los siguientes ejemplos para hacer uso de un mapper en typescript:
- Tanto al recibir como enviar un objeto entre el cliente y el servidor, la entidad se transforma en otra.
- Si desarrollamos una librería pública para terceros, interesará separar una representación pública de la entidad de otra privada.
- En general, cualquier intercambio de datos entre funcionalidades que requiera una transformación.
Manos al teclado
Para poder empezar a programar nuestro mapper crearemos una clase que realice solo esa funcionalidad atendiendo a la letra “S” de los principios SOLID, es decir, que solo tiene una responsabilidad: Mapear.
Si necesitamos mappers para varias entidades (o ya para cualquier proyecto), entonces será una clase abstracta que contendrá la estructura mínima y común para el resto de sus clases derivadas y estas derivadas contendrán la lógica del mapeo.
De esta manera, crearemos una interfaz tal que:
- Exista un método “transform” que realice el mapeo. Diferente al nombre de “map” para diferenciarlo del “map” de los arrays u operador de observables
- Este método se sobrecargará para soportar tanto una entidad como un array.
De acuerdo a nuestra especificación la interfaz queda de la siguiente manera:
export interface IMapperService<S, T> {
transform(entity: S): T;
transform(array: S[]): T[];
transform(entityOrArray: S | S[]): T | T[];
}
Esta interfaz utiliza la potencia de los genéricos para soportar cualquier tipo, donde:
- S de Source: El tipo del origen
- T de Target: El tipo del destino
Finalmente la clase abstracta que implementa esta interfaz:
export abstract class MapperService<S, T> implements IMapperService<S, T> {
protected abstract map(entity: S): T;
transform(entity: S): T;
transform(array: S[]): T[];
transform(entityOrArray: S | S[]): T | T[] {
return Array.isArray(entityOrArray) ?
entityOrArray.map((item: S) => this.map(item)) :
this.map(entityOrArray);
}
}
Esta clase abstracta define un método abstracto “map” para ser definido en la clase derivada que contendrá la lógica del mapeo.
Como hemos dicho antes, el servicio obliga a saber el tipo de entrada y salida, así que necesitaremos que estén definidos. Vamos a suponer un ejemplo en el que recibimos datos del servidor y queremos transformarlos para la aplicación, entonces sería:
Entidad de entrada (ó de lectura/servidor):
export interface IReadEntity {
id?: string;
type?: string;
name?: string;
birthDate?: string;
version?: number;
}
Para diferenciarlo de la entidad de la aplicación angular, hemos puesto “Read” al nombre, pero bien podría ser por ejemplo “IServerEntity”, depende del naming que se desee usar pero que quede clara su diferencia.
Entidad de salida (la que usará la aplicación angular):
export interface IEntity {
id?: string;
name?: string;
country?: string;
birthDate?: Date;
}
Algunos datos se mantienen igual, como el ‘id’ y ‘name’ pero hay otros que cambian como por ejemplo:
- country: Nueva propiedad.
- birthDate: Se transforma de string a tipo Date.
- type: Desaparece.
- version: Desaparece.
Finalmente nuestra clase que mapea quedaría de la siguiente manera:
@Injectable()
export class EntityMapperService extends MapperService<IReadEntity, IEntity> {
protected map(entity: IReadEntity): IEntity {
return {
id: entity.id,
name: entity.name,
country: '',
birthDate: new Date(entity.birthDate)
};
}
}
Ahora solo quedaría utilizarla. En el siguiente código inyectamos el servicio y en un componente y lo usamos en su inicialización (ngOnInit):
export class AppComponent implements OnInit {
title = 'mappers';
public constructor(private _entityMapperService: EntityMapperService) {
}
ngOnInit(): void {
// Map entity
const readEntity: IReadEntity = {
id: '123',
name: 'example mapper',
birthDate: '2000/01/01',
type: 'person',
version: 2
};
const entity: IEntity = this._entityMapperService.transform(readEntity);
// Map array of entities
const arrayEntities: Array<IReadEntity> = [readEntity];
const entities: Array<IEntity> = this._entityMapperService.transform(arrayEntities);
}
}
A efectos prácticos la entidad de lectura la hemos creado como un objeto literal, pero bien podría venir de un servicio http, de un observable, etcétera.
Si quieres ver ejemplos del uso de este mapper junto al operador ‘map’ de los observables, puedes leer el artículo Mapper en observables de Angular.
Puedes descargar el código del artículo en GitHub