Es la era ES6 y Typecript. Hoy en día trabajas con clases y objetos constructores más que nunca. Class-transformer le permite transformar un objeto simple en alguna instancia de clase y viceversa. También permite serializar/deserializar objetos según criterios. Esta herramienta es muy útil tanto en el frontend como en el backend.
Ejemplo de cómo utilizar con angular 2 en plunker. El código fuente está disponible aquí.
En JavaScript hay dos tipos de objetos:
Los objetos simples son objetos que son instancias de la clase Object
. A veces se les llama objetos literales , cuando se crean mediante notación {}
. Los objetos de clase son instancias de clases con constructores, propiedades y métodos propios definidos. Por lo general, los define mediante notación class
.
Entonces, ¿cuál es el problema?
A veces desea transformar un objeto simple de JavaScript a las clases de ES6 que tiene. Por ejemplo, si está cargando un json desde su backend, alguna API o desde un archivo json, y después de analizarlo JSON.parse
, tendrá un objeto javascript simple, no una instancia de la clase que tiene.
Por ejemplo, tienes una lista de usuarios en tu users.json
que estás cargando:
[
{
"id" : 1 ,
"firstName" : " Johny " ,
"lastName" : " Cage " ,
"age" : 27
},
{
"id" : 2 ,
"firstName" : " Ismoil " ,
"lastName" : " Somoni " ,
"age" : 50
},
{
"id" : 3 ,
"firstName" : " Luke " ,
"lastName" : " Dacascos " ,
"age" : 12
}
]
Y tienes una clase User
:
export class User {
id : number ;
firstName : string ;
lastName : string ;
age : number ;
getName ( ) {
return this . firstName + ' ' + this . lastName ;
}
isAdult ( ) {
return this . age > 36 && this . age < 60 ;
}
}
Está asumiendo que está descargando usuarios del tipo User
del archivo users.json
y es posible que desee escribir el siguiente código:
fetch ( 'users.json' ) . then ( ( users : User [ ] ) => {
// you can use users here, and type hinting also will be available to you,
// but users are not actually instances of User class
// this means that you can't use methods of User class
} ) ;
En este código puedes usar users[0].id
, también puedes usar users[0].firstName
y users[0].lastName
. Sin embargo, no puede utilizar users[0].getName()
o users[0].isAdult()
porque "usuarios" en realidad es una matriz de objetos javascript simples, no instancias del objeto Usuario. De hecho, le mintió al compilador cuando dijo que sus users: User[]
.
Entonces, ¿qué hacer? ¿Cómo crear una matriz de users
de instancias de objetos User
en lugar de objetos simples de JavaScript? La solución es crear nuevas instancias del objeto Usuario y copiar manualmente todas las propiedades a nuevos objetos. Pero las cosas pueden salir mal muy rápidamente una vez que se tiene una jerarquía de objetos más compleja.
¿Alternativas? Sí, puedes usar el transformador de clase. El propósito de esta biblioteca es ayudarlo a asignar sus objetos javascript simples a las instancias de clases que tiene.
Esta biblioteca también es excelente para los modelos expuestos en sus API, porque proporciona una excelente herramienta para controlar lo que sus modelos exponen en su API. Aquí hay un ejemplo de cómo se verá:
fetch ( 'users.json' ) . then ( ( users : Object [ ] ) => {
const realUsers = plainToInstance ( User , users ) ;
// now each user in realUsers is an instance of User class
} ) ;
Ahora puede utilizar los métodos users[0].getName()
y users[0].isAdult()
.
Instalar módulo:
npm install class-transformer --save
Se requiere el shim reflect-metadata
, instálelo también:
npm install reflect-metadata --save
y asegúrese de importarlo en un lugar global, como app.ts:
import 'reflect-metadata' ;
Se utilizan las funciones de ES6; si está utilizando una versión anterior de node.js, es posible que necesite instalar es6-shim:
npm install es6-shim --save
e importarlo en un lugar global como app.ts:
import 'es6-shim' ;
Instalar módulo:
npm install class-transformer --save
Se requiere el shim reflect-metadata
, instálelo también:
npm install reflect-metadata --save
agregue <script>
a reflect-metadata en el encabezado de su index.html
:
< html >
< head >
<!-- ... -->
< script src =" node_modules/reflect-metadata/Reflect.js " > </ script >
</ head >
<!-- ... -->
</ html >
Si está utilizando angular 2, ya debería tener esta cuña instalada.
Si está utilizando system.js, es posible que desee agregar esto a la configuración map
y package
:
{
"map" : {
"class-transformer" : " node_modules/class-transformer "
},
"packages" : {
"class-transformer" : { "main" : " index.js " , "defaultExtension" : " js " }
}
}
Este método transforma un objeto simple de JavaScript en una instancia de una clase específica.
import { plainToInstance } from 'class-transformer' ;
let users = plainToInstance ( User , userJson ) ; // to convert user plain object a single user. also supports arrays
Este método transforma un objeto simple en una instancia utilizando un Objeto ya lleno que es una instancia de la clase de destino.
const defaultUser = new User ( ) ;
defaultUser . role = 'user' ;
let mixedUser = plainToClassFromExist ( defaultUser , user ) ; // mixed user should have the value role = user when no value is set otherwise.
Este método transforma su objeto de clase nuevamente en un objeto javascript simple, que puede ser JSON.stringify
más adelante.
import { instanceToPlain } from 'class-transformer' ;
let photo = instanceToPlain ( photo ) ;
Este método transforma su objeto de clase en una nueva instancia del objeto de clase. Esto puede tratarse como una clonación profunda de sus objetos.
import { instanceToInstance } from 'class-transformer' ;
let photo = instanceToInstance ( photo ) ;
También puedes usar una opción ignoreDecorators
en las opciones de transformación para ignorar todos los decoradores que están usando tus clases.
Puede serializar su modelo directamente en json usando el método serialize
:
import { serialize } from 'class-transformer' ;
let photo = serialize ( photo ) ;
serialize
funciona tanto con matrices como con no matrices.
Puedes deserializar tu modelo desde json usando el método deserialize
:
import { deserialize } from 'class-transformer' ;
let photo = deserialize ( Photo , photo ) ;
Para que la deserialización funcione con matrices, utilice el método deserializeArray
:
import { deserializeArray } from 'class-transformer' ;
let photos = deserializeArray ( Photo , photos ) ;
El comportamiento predeterminado del método plainToInstance
es establecer todas las propiedades del objeto simple, incluso aquellas que no están especificadas en la clase.
import { plainToInstance } from 'class-transformer' ;
class User {
id : number ;
firstName : string ;
lastName : string ;
}
const fromPlainUser = {
unkownProp : 'hello there' ,
firstName : 'Umed' ,
lastName : 'Khudoiberdiev' ,
} ;
console . log ( plainToInstance ( User , fromPlainUser ) ) ;
// User {
// unkownProp: 'hello there',
// firstName: 'Umed',
// lastName: 'Khudoiberdiev',
// }
Si este comportamiento no se adapta a sus necesidades, puede utilizar la opción excludeExtraneousValues
en el método plainToInstance
mientras expone todas las propiedades de su clase como requisito.
import { Expose , plainToInstance } from 'class-transformer' ;
class User {
@ Expose ( ) id : number ;
@ Expose ( ) firstName : string ;
@ Expose ( ) lastName : string ;
}
const fromPlainUser = {
unkownProp : 'hello there' ,
firstName : 'Umed' ,
lastName : 'Khudoiberdiev' ,
} ;
console . log ( plainToInstance ( User , fromPlainUser , { excludeExtraneousValues : true } ) ) ;
// User {
// id: undefined,
// firstName: 'Umed',
// lastName: 'Khudoiberdiev'
// }
Cuando intenta transformar objetos que tienen objetos anidados, es necesario saber qué tipo de objeto está intentando transformar. Dado que Typecript aún no tiene buenas capacidades de reflexión, deberíamos especificar implícitamente qué tipo de objeto contiene cada propiedad. Esto se hace usando el decorador @Type
.
Digamos que tenemos un álbum con fotos. Y estamos intentando convertir el objeto simple del álbum en un objeto de clase:
import { Type , plainToInstance } from 'class-transformer' ;
export class Album {
id : number ;
name : string ;
@ Type ( ( ) => Photo )
photos : Photo [ ] ;
}
export class Photo {
id : number ;
filename : string ;
}
let album = plainToInstance ( Album , albumJson ) ;
// now album is Album object with Photo objects inside
En caso de que el objeto anidado pueda ser de diferentes tipos, puede proporcionar un objeto de opciones adicional que especifique un discriminador. La opción discriminadora debe definir una property
que contenga el nombre del subtipo del objeto y los posibles subTypes
a los que se puede convertir el objeto anidado. Un subtipo tiene un value
, que contiene el constructor del tipo y el name
, que puede coincidir con la property
del discriminador.
Digamos que tenemos un álbum que tiene una foto superior. Pero esta foto puede ser de ciertos tipos diferentes. Y estamos intentando convertir el objeto simple del álbum en un objeto de clase. La entrada del objeto simple tiene que definir la propiedad adicional __type
. Esta propiedad se elimina durante la transformación de forma predeterminada:
Entrada JSON :
{
"id" : 1 ,
"name" : " foo " ,
"topPhoto" : {
"id" : 9 ,
"filename" : " cool_wale.jpg " ,
"depth" : 1245 ,
"__type" : " underwater "
}
}
import { Type , plainToInstance } from 'class-transformer' ;
export abstract class Photo {
id : number ;
filename : string ;
}
export class Landscape extends Photo {
panorama : boolean ;
}
export class Portrait extends Photo {
person : Person ;
}
export class UnderWater extends Photo {
depth : number ;
}
export class Album {
id : number ;
name : string ;
@ Type ( ( ) => Photo , {
discriminator : {
property : '__type' ,
subTypes : [
{ value : Landscape , name : 'landscape' } ,
{ value : Portrait , name : 'portrait' } ,
{ value : UnderWater , name : 'underwater' } ,
] ,
} ,
} )
topPhoto : Landscape | Portrait | UnderWater ;
}
let album = plainToInstance ( Album , albumJson ) ;
// now album is Album object with a UnderWater object without `__type` property.
Sugerencia: Lo mismo se aplica a matrices con diferentes subtipos. Además, puede especificar keepDiscriminatorProperty: true
en las opciones para mantener la propiedad discriminadora también dentro de la clase resultante.
Puede exponer lo que devuelve su captador o método estableciendo un decorador @Expose()
para esos captadores o métodos:
import { Expose } from 'class-transformer' ;
export class User {
id : number ;
firstName : string ;
lastName : string ;
password : string ;
@ Expose ( )
get name ( ) {
return this . firstName + ' ' + this . lastName ;
}
@ Expose ( )
getFullName ( ) {
return this . firstName + ' ' + this . lastName ;
}
}
Si desea exponer algunas de las propiedades con un nombre diferente, puede hacerlo especificando una opción name
en @Expose
decorador:
import { Expose } from 'class-transformer' ;
export class User {
@ Expose ( { name : 'uid' } )
id : number ;
firstName : string ;
lastName : string ;
@ Expose ( { name : 'secretKey' } )
password : string ;
@ Expose ( { name : 'fullName' } )
getFullName ( ) {
return this . firstName + ' ' + this . lastName ;
}
}
A veces desea omitir algunas propiedades durante la transformación. Esto se puede hacer usando el decorador @Exclude
:
import { Exclude } from 'class-transformer' ;
export class User {
id : number ;
email : string ;
@ Exclude ( )
password : string ;
}
Ahora, cuando transforme un usuario, la propiedad password
se omitirá y no se incluirá en el resultado transformado.
Puedes controlar en qué operación excluirás una propiedad. Utilice las opciones toClassOnly
o toPlainOnly
:
import { Exclude } from 'class-transformer' ;
export class User {
id : number ;
email : string ;
@ Exclude ( { toPlainOnly : true } )
password : string ;
}
Ahora la propiedad password
se excluirá solo durante la operación instanceToPlain
. Viceversa, utilice la opción toClassOnly
.
Puede omitir todas las propiedades de la clase y exponer solo aquellas que sean necesarias explícitamente:
import { Exclude , Expose } from 'class-transformer' ;
@ Exclude ( )
export class User {
@ Expose ( )
id : number ;
@ Expose ( )
email : string ;
password : string ;
}
Ahora id
y email
quedarán expuestos y la contraseña se excluirá durante la transformación. Alternativamente, puede establecer una estrategia de exclusión durante la transformación:
import { instanceToPlain } from 'class-transformer' ;
let photo = instanceToPlain ( photo , { strategy : 'excludeAll' } ) ;
En este caso no es necesario @Exclude()
una clase completa.
Si nombras tus propiedades privadas con un prefijo, digamos con _
, entonces también puedes excluir dichas propiedades de la transformación:
import { instanceToPlain } from 'class-transformer' ;
let photo = instanceToPlain ( photo , { excludePrefixes : [ '_' ] } ) ;
Esto omitirá todas las propiedades que comiencen con el prefijo _
. Puede pasar cualquier número de prefijos y todas las propiedades que comiencen con estos prefijos se ignorarán. Por ejemplo:
import { Expose , instanceToPlain } from 'class-transformer' ;
export class User {
id : number ;
private _firstName : string ;
private _lastName : string ;
_password : string ;
setName ( firstName : string , lastName : string ) {
this . _firstName = firstName ;
this . _lastName = lastName ;
}
@ Expose ( )
get name ( ) {
return this . _firstName + ' ' + this . _lastName ;
}
}
const user = new User ( ) ;
user . id = 1 ;
user . setName ( 'Johny' , 'Cage' ) ;
user . _password = '123' ;
const plainUser = instanceToPlain ( user , { excludePrefixes : [ '_' ] } ) ;
// here plainUser will be equal to
// { id: 1, name: "Johny Cage" }
Puede utilizar grupos para controlar qué datos estarán expuestos y cuáles no:
import { Exclude , Expose , instanceToPlain } from 'class-transformer' ;
export class User {
id : number ;
name : string ;
@ Expose ( { groups : [ 'user' , 'admin' ] } ) // this means that this data will be exposed only to users and admins
email : string ;
@ Expose ( { groups : [ 'user' ] } ) // this means that this data will be exposed only to users
password : string ;
}
let user1 = instanceToPlain ( user , { groups : [ 'user' ] } ) ; // will contain id, name, email and password
let user2 = instanceToPlain ( user , { groups : [ 'admin' ] } ) ; // will contain id, name and email
Si está creando una API que tiene diferentes versiones, class-transformer tiene herramientas extremadamente útiles para eso. Puede controlar qué propiedades de su modelo deben exponerse o excluirse en qué versión. Ejemplo:
import { Exclude , Expose , instanceToPlain } from 'class-transformer' ;
export class User {
id : number ;
name : string ;
@ Expose ( { since : 0.7 , until : 1 } ) // this means that this property will be exposed for version starting from 0.7 until 1
email : string ;
@ Expose ( { since : 2.1 } ) // this means that this property will be exposed for version starting from 2.1
password : string ;
}
let user1 = instanceToPlain ( user , { version : 0.5 } ) ; // will contain id and name
let user2 = instanceToPlain ( user , { version : 0.7 } ) ; // will contain id, name and email
let user3 = instanceToPlain ( user , { version : 1 } ) ; // will contain id and name
let user4 = instanceToPlain ( user , { version : 2 } ) ; // will contain id and name
let user5 = instanceToPlain ( user , { version : 2.1 } ) ; // will contain id, name and password
A veces tienes una Fecha en tu objeto javascript simple recibido en formato de cadena. Y desea crear un objeto de fecha javascript real a partir de él. Puedes hacerlo simplemente pasando un objeto Date al decorador @Type
:
import { Type } from 'class-transformer' ;
export class User {
id : number ;
email : string ;
password : string ;
@ Type ( ( ) => Date )
registrationDate : Date ;
}
Se puede utilizar la misma técnica con los tipos primitivos Number
, String
Boolean
cuando desee convertir sus valores en estos tipos.
Cuando utiliza matrices, debe proporcionar un tipo de objeto que contiene la matriz. Este tipo, lo especificas en un decorador @Type()
:
import { Type } from 'class-transformer' ;
export class Photo {
id : number ;
name : string ;
@ Type ( ( ) => Album )
albums : Album [ ] ;
}
También puede utilizar tipos de matrices personalizados:
import { Type } from 'class-transformer' ;
export class AlbumCollection extends Array < Album > {
// custom array functions ...
}
export class Photo {
id : number ;
name : string ;
@ Type ( ( ) => Album )
albums : AlbumCollection ;
}
La biblioteca manejará la transformación adecuada automáticamente.
Las colecciones de ES6 Set
y Map
también requieren el decorador @Type
:
export class Skill {
name : string ;
}
export class Weapon {
name : string ;
range : number ;
}
export class Player {
name : string ;
@ Type ( ( ) => Skill )
skills : Set < Skill > ;
@ Type ( ( ) => Weapon )
weapons : Map < string , Weapon > ;
}
Puede realizar una transformación de datos adicional utilizando el decorador @Transform
. Por ejemplo, desea que su objeto Date
sea un objeto moment
cuando transforma un objeto de plano a clase:
import { Transform } from 'class-transformer' ;
import * as moment from 'moment' ;
import { Moment } from 'moment' ;
export class Photo {
id : number ;
@ Type ( ( ) => Date )
@ Transform ( ( { value } ) => moment ( value ) , { toClassOnly : true } )
date : Moment ;
}
Ahora, cuando llame a plainToInstance
y envíe una representación simple del objeto Foto, convertirá un valor de fecha en su objeto fotográfico a la fecha del momento. @Transform
decorador también admite grupos y control de versiones.
El decorador @Transform
recibe más argumentos para permitirle configurar cómo desea que se realice la transformación.
@ Transform ( ( { value , key , obj , type } ) => value )
Argumento | Descripción |
---|---|
value | El valor de la propiedad antes de la transformación. |
key | El nombre de la propiedad transformada. |
obj | El objeto fuente de transformación. |
type | El tipo de transformación. |
options | El objeto de opciones pasado al método de transformación. |
Firma | Ejemplo | Descripción |
---|---|---|
@TransformClassToPlain | @TransformClassToPlain({ groups: ["user"] }) | Transforme el retorno del método con instanciaToPlain y exponga las propiedades de la clase. |
@TransformClassToClass | @TransformClassToClass({ groups: ["user"] }) | Transforme el retorno del método con instanciaToInstance y exponga las propiedades de la clase. |
@TransformPlainToClass | @TransformPlainToClass(User, { groups: ["user"] }) | Transforme el retorno del método con PlainToInstance y exponga las propiedades de la clase. |
Los decoradores anteriores aceptan un argumento opcional: ClassTransformOptions: las opciones de transformación como grupos, versión, nombre.
Un ejemplo:
@ Exclude ( )
class User {
id : number ;
@ Expose ( )
firstName : string ;
@ Expose ( )
lastName : string ;
@ Expose ( { groups : [ 'user.email' ] } )
email : string ;
password : string ;
}
class UserController {
@ TransformClassToPlain ( { groups : [ 'user.email' ] } )
getUser ( ) {
const user = new User ( ) ;
user . firstName = 'Snir' ;
user . lastName = 'Segal' ;
user . password = 'imnosuperman' ;
return user ;
}
}
const controller = new UserController ( ) ;
const user = controller . getUser ( ) ;
la variable user
contendrá solo las propiedades de nombre, apellido y correo electrónico porque son las variables expuestas. La propiedad del correo electrónico también está expuesta porque mencionamos el grupo "usuario.email".
Los genéricos no son compatibles porque TypeScript aún no tiene buenas capacidades de reflexión. Una vez que el equipo de TypeScript nos proporcione mejores herramientas de reflexión de tipos en tiempo de ejecución, se implementarán los genéricos. Sin embargo, hay algunos ajustes que puede utilizar y que tal vez puedan resolver su problema. Mira este ejemplo.
NOTA Si utiliza el validador de clases junto con el transformador de clases, probablemente NO desee habilitar esta función.
Habilita la conversión automática entre tipos integrados según la información de tipo proporcionada por Typecript. Deshabilitado por defecto.
import { IsString } from 'class-validator' ;
class MyPayload {
@ IsString ( )
prop : string ;
}
const result1 = plainToInstance ( MyPayload , { prop : 1234 } , { enableImplicitConversion : true } ) ;
const result2 = plainToInstance ( MyPayload , { prop : 1234 } , { enableImplicitConversion : false } ) ;
/**
* result1 will be `{ prop: "1234" }` - notice how the prop value has been converted to string.
* result2 will be `{ prop: 1234 }` - default behaviour
*/
Se ignoran las referencias circulares. Por ejemplo, si está transformando la clase User
que contiene photos
de propiedades con el tipo Photo
y Photo
contiene un enlace user
a su User
principal, entonces user
será ignorado durante la transformación. Las referencias circulares no se ignoran sólo durante la operación instanceToInstance
.
Digamos que desea descargar usuarios y desea que se asignen automáticamente a las instancias de la clase User
.
import { plainToInstance } from 'class-transformer' ;
this . http
. get ( 'users.json' )
. map ( res => res . json ( ) )
. map ( res => plainToInstance ( User , res as Object [ ] ) )
. subscribe ( users => {
// now "users" is type of User[] and each user has getName() and isAdult() methods available
console . log ( users ) ;
} ) ;
También puede inyectar una clase ClassTransformer
como servicio en providers
y utilizar sus métodos.
Ejemplo de cómo utilizar con angular 2 en plunker. El código fuente está aquí.
Eche un vistazo a los ejemplos en ./sample para ver más ejemplos de usos.
Consulte información sobre cambios importantes y notas de la versión aquí.