Sua era ES6 e Typescript. Hoje em dia você está trabalhando com classes e objetos construtores mais do que nunca. O transformador de classe permite transformar um objeto simples em alguma instância de classe e vice-versa. Também permite serializar/desserializar objetos com base em critérios. Esta ferramenta é muito útil tanto no frontend quanto no backend.
Exemplo de como usar com angular 2 no plunker. O código fonte está disponível aqui.
Em JavaScript existem dois tipos de objetos:
Objetos simples são objetos que são instâncias da classe Object
. Às vezes, eles são chamados de objetos literais , quando criados por meio da notação {}
. Objetos de classe são instâncias de classes com construtor, propriedades e métodos próprios definidos. Normalmente você os define por meio de notação class
.
Então, qual é o problema?
Às vezes você deseja transformar um objeto javascript simples nas classes ES6 que você possui. Por exemplo, se você estiver carregando um json de seu back-end, alguma API ou de um arquivo json, e depois de JSON.parse
, você terá um objeto javascript simples, não uma instância de classe que possui.
Por exemplo, você tem uma lista de usuários em users.json
que está carregando:
[
{
"id" : 1 ,
"firstName" : " Johny " ,
"lastName" : " Cage " ,
"age" : 27
},
{
"id" : 2 ,
"firstName" : " Ismoil " ,
"lastName" : " Somoni " ,
"age" : 50
},
{
"id" : 3 ,
"firstName" : " Luke " ,
"lastName" : " Dacascos " ,
"age" : 12
}
]
E você tem uma classe 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 ;
}
}
Você está assumindo que está baixando usuários do tipo User
do arquivo users.json
e pode querer escrever o seguinte 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
} ) ;
Neste código você pode usar users[0].id
, você também pode usar users[0].firstName
e users[0].lastName
. No entanto, você não pode usar users[0].getName()
ou users[0].isAdult()
porque "users" na verdade é uma matriz de objetos javascript simples, não instâncias do objeto User. Na verdade, você mentiu para o compilador quando disse que seus users: User[]
.
Então, o que fazer? Como criar uma matriz de users
com instâncias de objetos User
em vez de objetos javascript simples? A solução é criar novas instâncias do objeto User e copiar manualmente todas as propriedades para novos objetos. Mas as coisas podem dar errado muito rapidamente quando você tiver uma hierarquia de objetos mais complexa.
Alternativas? Sim, você pode usar o transformador de classe. O objetivo desta biblioteca é ajudá-lo a mapear seus objetos javascript simples para as instâncias de classes que você possui.
Essa biblioteca também é ótima para modelos expostos em suas APIs, pois fornece uma ótima ferramenta para controlar o que seus modelos estão expondo em sua API. Aqui está um exemplo de como ficará:
fetch ( 'users.json' ) . then ( ( users : Object [ ] ) => {
const realUsers = plainToInstance ( User , users ) ;
// now each user in realUsers is an instance of User class
} ) ;
Agora você pode usar os métodos users[0].getName()
e users[0].isAdult()
.
Instalar módulo:
npm install class-transformer --save
O shim reflect-metadata
é necessário, instale-o também:
npm install reflect-metadata --save
e certifique-se de importá-lo em um local global, como app.ts:
import 'reflect-metadata' ;
Recursos ES6 são usados, se você estiver usando uma versão antiga do node.js, pode ser necessário instalar o es6-shim:
npm install es6-shim --save
e importe-o em um local global como app.ts:
import 'es6-shim' ;
Instalar módulo:
npm install class-transformer --save
O shim reflect-metadata
é necessário, instale-o também:
npm install reflect-metadata --save
adicione <script>
a reflect-metadata no cabeçalho do seu index.html
:
< html >
< head >
<!-- ... -->
< script src =" node_modules/reflect-metadata/Reflect.js " > </ script >
</ head >
<!-- ... -->
</ html >
Se você estiver usando o angular 2, você já deve ter esse calço instalado.
Se você estiver usando system.js, você pode querer adicionar isto ao map
e à configuração package
:
{
"map" : {
"class-transformer" : " node_modules/class-transformer "
},
"packages" : {
"class-transformer" : { "main" : " index.js " , "defaultExtension" : " js " }
}
}
Este método transforma um objeto javascript simples em uma instância de uma classe 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 um objeto simples em uma instância usando um Objeto já preenchido que é uma instância da classe alvo.
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 seu objeto de classe de volta em um objeto javascript simples, que pode ser JSON.stringify
posteriormente.
import { instanceToPlain } from 'class-transformer' ;
let photo = instanceToPlain ( photo ) ;
Este método transforma seu objeto de classe em uma nova instância do objeto de classe. Isso pode ser tratado como um clone profundo de seus objetos.
import { instanceToInstance } from 'class-transformer' ;
let photo = instanceToInstance ( photo ) ;
Você também pode usar uma opção ignoreDecorators
nas opções de transformação para ignorar todos os decoradores que suas classes estão usando.
Você pode serializar seu modelo diretamente para JSON usando o método serialize
:
import { serialize } from 'class-transformer' ;
let photo = serialize ( photo ) ;
serialize
funciona com arrays e não arrays.
Você pode desserializar seu modelo do json usando o método deserialize
:
import { deserialize } from 'class-transformer' ;
let photo = deserialize ( Photo , photo ) ;
Para fazer a desserialização funcionar com arrays, use o método deserializeArray
:
import { deserializeArray } from 'class-transformer' ;
let photos = deserializeArray ( Photo , photos ) ;
O comportamento padrão do método plainToInstance
é definir todas as propriedades do objeto simples, mesmo aquelas que não são especificadas na classe.
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',
// }
Se esse comportamento não atender às suas necessidades, você poderá usar a opção excludeExtraneousValues
no método plainToInstance
enquanto expõe todas as propriedades da sua classe como um 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'
// }
Quando você está tentando transformar objetos que possuem objetos aninhados, é necessário saber que tipo de objeto você está tentando transformar. Como o Typescript ainda não possui boas habilidades de reflexão, devemos especificar implicitamente que tipo de objeto cada propriedade contém. Isso é feito usando o decorador @Type
.
Digamos que temos um álbum com fotos. E estamos tentando converter o objeto simples do álbum em objeto de classe:
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
Caso o objeto aninhado possa ser de tipos diferentes, você pode fornecer um objeto de opções adicional, que especifica um discriminador. A opção discriminadora deve definir uma property
que contenha o nome do subtipo do objeto e os possíveis subTypes
para os quais o objeto aninhado pode ser convertido. Um subtipo possui um value
, que contém o construtor do Type e o name
, que pode corresponder à property
do discriminador.
Digamos que temos um álbum que tem uma foto principal. Mas esta foto pode ser de alguns tipos diferentes. E estamos tentando converter o objeto simples do álbum em objeto de classe. A entrada do objeto simples deve definir a propriedade adicional __type
. Esta propriedade é removida durante a transformação por padrão:
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.
Dica: O mesmo se aplica a arrays com diferentes subtipos. Além disso, você pode especificar keepDiscriminatorProperty: true
nas opções para manter a propriedade discriminadora também dentro da classe resultante.
Você pode expor o que seu getter ou método retorna definindo um decorador @Expose()
para esses getters ou 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 ;
}
}
Se quiser expor algumas das propriedades com um nome diferente, você pode fazer isso especificando uma opção name
para @Expose
decorator:
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 ;
}
}
Às vezes você deseja pular algumas propriedades durante a transformação. Isso pode ser feito usando o decorador @Exclude
:
import { Exclude } from 'class-transformer' ;
export class User {
id : number ;
email : string ;
@ Exclude ( )
password : string ;
}
Agora ao transformar um Usuário, a propriedade password
será ignorada e não será incluída no resultado transformado.
Você pode controlar em qual operação excluirá uma propriedade. Use as opções toClassOnly
ou toPlainOnly
:
import { Exclude } from 'class-transformer' ;
export class User {
id : number ;
email : string ;
@ Exclude ( { toPlainOnly : true } )
password : string ;
}
Agora a propriedade password
será excluída apenas durante a operação instanceToPlain
. Vice-versa, use a opção toClassOnly
.
Você pode pular todas as propriedades da classe e expor explicitamente apenas aquelas que são necessárias:
import { Exclude , Expose } from 'class-transformer' ;
@ Exclude ( )
export class User {
@ Expose ( )
id : number ;
@ Expose ( )
email : string ;
password : string ;
}
Agora id
e email
serão expostos e a senha será excluída durante a transformação. Alternativamente, você pode definir a estratégia de exclusão durante a transformação:
import { instanceToPlain } from 'class-transformer' ;
let photo = instanceToPlain ( photo , { strategy : 'excludeAll' } ) ;
Neste caso você não precisa @Exclude()
uma classe inteira.
Se você nomear suas propriedades privadas com um prefixo, digamos com _
, também poderá excluir essas propriedades da transformação:
import { instanceToPlain } from 'class-transformer' ;
let photo = instanceToPlain ( photo , { excludePrefixes : [ '_' ] } ) ;
Isso ignorará todas as propriedades que começam com o prefixo _
. Você pode passar qualquer número de prefixos e todas as propriedades que começam com esses prefixos serão ignoradas. Por exemplo:
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" }
Você pode usar grupos para controlar quais dados serão expostos e quais não serão:
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
Se você está construindo uma API que possui versões diferentes, o class-transformer possui ferramentas extremamente úteis para isso. Você pode controlar quais propriedades do seu modelo devem ser expostas ou excluídas em qual versão. Exemplo:
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
Às vezes você tem uma data em seu objeto javascript simples recebido em formato de string. E você deseja criar um objeto Date javascript real a partir dele. Você pode fazer isso simplesmente passando um objeto Date para o decorador @Type
:
import { Type } from 'class-transformer' ;
export class User {
id : number ;
email : string ;
password : string ;
@ Type ( ( ) => Date )
registrationDate : Date ;
}
A mesma técnica pode ser usada com os tipos primitivos Number
, String
, Boolean
quando você deseja converter seus valores nesses tipos.
Ao usar arrays, você deve fornecer um tipo de objeto que o array contém. Este tipo você especifica em um decorador @Type()
:
import { Type } from 'class-transformer' ;
export class Photo {
id : number ;
name : string ;
@ Type ( ( ) => Album )
albums : Album [ ] ;
}
Você também pode usar tipos de array 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 ;
}
A biblioteca tratará da transformação adequada automaticamente.
As coleções ES6 Set
e Map
também requerem o 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 > ;
}
Você pode realizar transformações adicionais de dados usando o decorador @Transform
. Por exemplo, você deseja fazer com que seu objeto Date
seja um objeto moment
quando você estiver transformando um objeto simples em classe:
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 ;
}
Agora, quando você chamar plainToInstance
e enviar uma representação simples do objeto Photo, ele converterá um valor de data em seu objeto Photo para a data do momento. O decorador @Transform
também oferece suporte a grupos e controle de versão.
O decorador @Transform
recebe mais argumentos para permitir que você configure como deseja que a transformação seja feita.
@ Transform ( ( { value , key , obj , type } ) => value )
Argumento | Descrição |
---|---|
value | O valor da propriedade antes da transformação. |
key | O nome da propriedade transformada. |
obj | O objeto de origem da transformação. |
type | O tipo de transformação. |
options | O objeto de opções passado para o método de transformação. |
Assinatura | Exemplo | Descrição |
---|---|---|
@TransformClassToPlain | @TransformClassToPlain({ groups: ["user"] }) | Transforme o retorno do método com instanceToPlain e exponha as propriedades da classe. |
@TransformClassToClass | @TransformClassToClass({ groups: ["user"] }) | Transforme o retorno do método com instanceToInstance e exponha as propriedades da classe. |
@TransformPlainToClass | @TransformPlainToClass(User, { groups: ["user"] }) | Transforme o retorno do método com plainToInstance e exponha as propriedades da classe. |
Os decoradores acima aceitam um argumento opcional: ClassTransformOptions - As opções de transformação como grupos, versão, nome
Um exemplo:
@ 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 ( ) ;
a variável user
conterá apenas as propriedades firstName, lastName e email porque são as variáveis expostas. A propriedade email também está exposta porque mencionamos o grupo "user.email".
Os genéricos não são suportados porque o TypeScript ainda não possui boas habilidades de reflexão. Assim que a equipe do TypeScript nos fornecer melhores ferramentas de reflexão de tipo de tempo de execução, os genéricos serão implementados. Existem alguns ajustes que você pode usar, que talvez possam resolver seu problema. Confira este exemplo.
NOTA Se você usar o validador de classe junto com o transformador de classe, provavelmente NÃO deseja habilitar esta função.
Permite a conversão automática entre tipos integrados com base nas informações de tipo fornecidas pelo Typescript. Desativado por padrão.
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
*/
As referências circulares são ignoradas. Por exemplo, se você estiver transformando a classe User
que contém a propriedade photos
com o tipo Photo
, e Photo
contém o link user
para seu pai User
, user
será ignorado durante a transformação. As referências circulares não são ignoradas apenas durante a operação instanceToInstance
.
Digamos que você queira baixar usuários e que eles sejam mapeados automaticamente para as instâncias da classe 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 ) ;
} ) ;
Você também pode injetar uma classe ClassTransformer
como serviço em providers
e usar seus métodos.
Exemplo de como usar com angular 2 no plunker. O código fonte está aqui.
Dê uma olhada nos exemplos em ./sample para mais exemplos de usos.
Veja informações sobre alterações importantes e notas de lançamento aqui.