C’est l’ère ES6 et Typescript. De nos jours, vous travaillez plus que jamais avec des classes et des objets constructeur. Class-transformer vous permet de transformer un objet simple en une instance de classe et versa. Il permet également de sérialiser/désérialiser un objet en fonction de critères. Cet outil est très utile à la fois en frontend et en backend.
Exemple d'utilisation avec angulaire 2 dans Plunker. Le code source est disponible ici.
En JavaScript, il existe deux types d'objets :
Les objets simples sont des objets qui sont des instances de la classe Object
. Parfois, ils sont appelés objets littéraux , lorsqu'ils sont créés via la notation {}
. Les objets de classe sont des instances de classes avec leurs propres constructeurs, propriétés et méthodes définis. Habituellement, vous les définissez via la notation class
.
Alors, quel est le problème ?
Parfois, vous souhaitez transformer un objet javascript simple en classes ES6 dont vous disposez. Par exemple, si vous chargez un json à partir de votre backend, d'une API ou d'un fichier json, et après l'avoir JSON.parse
vous avez un objet javascript simple, pas une instance de classe que vous avez.
Par exemple, vous avez une liste d'utilisateurs dans votre users.json
que vous chargez :
[
{
"id" : 1 ,
"firstName" : " Johny " ,
"lastName" : " Cage " ,
"age" : 27
},
{
"id" : 2 ,
"firstName" : " Ismoil " ,
"lastName" : " Somoni " ,
"age" : 50
},
{
"id" : 3 ,
"firstName" : " Luke " ,
"lastName" : " Dacascos " ,
"age" : 12
}
]
Et vous avez une 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 ;
}
}
Vous supposez que vous téléchargez des utilisateurs de type User
à partir du fichier users.json
et que vous souhaiterez peut-être écrire le code suivant :
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
} ) ;
Dans ce code, vous pouvez utiliser users[0].id
, vous pouvez également utiliser users[0].firstName
et users[0].lastName
. Cependant, vous ne pouvez pas utiliser users[0].getName()
ou users[0].isAdult()
car "users" est en fait un tableau d'objets javascript simples, et non des instances d'objet User. En fait, vous avez menti au compilateur lorsque vous avez dit que ses users: User[]
.
Alors que faire ? Comment créer un tableau users
d'instances d'objets User
au lieu d'objets javascript simples ? La solution consiste à créer de nouvelles instances d'objet utilisateur et à copier manuellement toutes les propriétés dans de nouveaux objets. Mais les choses peuvent mal tourner très rapidement une fois que vous disposez d’une hiérarchie d’objets plus complexe.
Alternatives ? Oui, vous pouvez utiliser un transformateur de classe. Le but de cette bibliothèque est de vous aider à mapper vos objets javascript simples aux instances de classes dont vous disposez.
Cette bibliothèque est également idéale pour les modèles exposés dans vos API, car elle fournit un excellent outil pour contrôler ce que vos modèles exposent dans votre API. Voici un exemple de ce à quoi cela ressemblera :
fetch ( 'users.json' ) . then ( ( users : Object [ ] ) => {
const realUsers = plainToInstance ( User , users ) ;
// now each user in realUsers is an instance of User class
} ) ;
Vous pouvez désormais utiliser les méthodes users[0].getName()
et users[0].isAdult()
.
Module d'installation :
npm install class-transformer --save
La cale reflect-metadata
est requise, installez-la également :
npm install reflect-metadata --save
et assurez-vous de l'importer dans un endroit global, comme app.ts :
import 'reflect-metadata' ;
Les fonctionnalités ES6 sont utilisées, si vous utilisez l'ancienne version de node.js, vous devrez peut-être installer es6-shim :
npm install es6-shim --save
et importez-le dans un endroit global comme app.ts :
import 'es6-shim' ;
Module d'installation :
npm install class-transformer --save
La cale reflect-metadata
est requise, installez-la également :
npm install reflect-metadata --save
ajoutez <script>
à think-metadata dans l'en-tête de votre index.html
:
< html >
< head >
<!-- ... -->
< script src =" node_modules/reflect-metadata/Reflect.js " > </ script >
</ head >
<!-- ... -->
</ html >
Si vous utilisez angulaire 2, vous devriez déjà avoir installé cette cale.
Si vous utilisez system.js, vous souhaiterez peut-être ajouter ceci dans la configuration map
et package
:
{
"map" : {
"class-transformer" : " node_modules/class-transformer "
},
"packages" : {
"class-transformer" : { "main" : " index.js " , "defaultExtension" : " js " }
}
}
Cette méthode transforme un objet javascript simple en instance d'une classe spécifique.
import { plainToInstance } from 'class-transformer' ;
let users = plainToInstance ( User , userJson ) ; // to convert user plain object a single user. also supports arrays
Cette méthode transforme un objet simple en une instance en utilisant un objet déjà rempli qui est une instance de la classe cible.
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.
Cette méthode transforme votre objet de classe en objet javascript simple, qui peut être JSON.stringify
plus tard.
import { instanceToPlain } from 'class-transformer' ;
let photo = instanceToPlain ( photo ) ;
Cette méthode transforme votre objet de classe en une nouvelle instance de l'objet de classe. Cela peut être traité comme un clonage profond de vos objets.
import { instanceToInstance } from 'class-transformer' ;
let photo = instanceToInstance ( photo ) ;
Vous pouvez également utiliser une option ignoreDecorators
dans les options de transformation pour ignorer tous les décorateurs utilisés par vos classes.
Vous pouvez sérialiser votre modèle directement en json en utilisant la méthode serialize
:
import { serialize } from 'class-transformer' ;
let photo = serialize ( photo ) ;
serialize
fonctionne avec les tableaux et les non-tableaux.
Vous pouvez désérialiser votre modèle depuis json en utilisant la méthode deserialize
:
import { deserialize } from 'class-transformer' ;
let photo = deserialize ( Photo , photo ) ;
Pour que la désérialisation fonctionne avec des tableaux, utilisez la méthode deserializeArray
:
import { deserializeArray } from 'class-transformer' ;
let photos = deserializeArray ( Photo , photos ) ;
Le comportement par défaut de la méthode plainToInstance
consiste à définir toutes les propriétés de l'objet plain, même celles qui ne sont pas spécifiées dans la 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',
// }
Si ce comportement ne répond pas à vos besoins, vous pouvez utiliser l'option excludeExtraneousValues
dans la méthode plainToInstance
tout en exposant toutes vos propriétés de classe comme exigence.
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'
// }
Lorsque vous essayez de transformer des objets contenant des objets imbriqués, il est nécessaire de savoir quel type d'objet vous essayez de transformer. Étant donné que Typescript n'a pas encore de bonnes capacités de réflexion, nous devons implicitement spécifier quel type d'objet contient chaque propriété. Cela se fait à l'aide du décorateur @Type
.
Disons que nous avons un album avec des photos. Et nous essayons de convertir un objet album plain en objet 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
Dans le cas où l'objet imbriqué peut être de différents types, vous pouvez fournir un objet d'options supplémentaire, qui spécifie un discriminateur. L'option discriminateur doit définir une property
qui contient le nom du sous-type de l'objet et les subTypes
possibles vers lesquels l'objet imbriqué peut être converti. Un sous-type a une value
, qui contient le constructeur du Type et le name
, qui peut correspondre à la property
du discriminateur.
Disons que nous avons un album qui contient une photo du haut. Mais cette photo peut être de différents types. Et nous essayons de convertir un objet simple album en objet de classe. L'entrée d'objet simple doit définir la propriété supplémentaire __type
. Cette propriété est supprimée lors de la transformation par défaut :
Entrée 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.
Astuce : la même chose s'applique aux tableaux avec des sous-types différents. De plus, vous pouvez spécifier keepDiscriminatorProperty: true
dans les options pour conserver la propriété discriminateur également dans votre classe résultante.
Vous pouvez exposer ce que renvoie votre getter ou votre méthode en définissant un décorateur @Expose()
sur ces getters ou méthodes :
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 vous souhaitez exposer certaines propriétés avec un nom différent, vous pouvez le faire en spécifiant une option name
au décorateur @Expose
:
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 ;
}
}
Parfois, vous souhaitez ignorer certaines propriétés lors de la transformation. Cela peut être fait en utilisant le décorateur @Exclude
:
import { Exclude } from 'class-transformer' ;
export class User {
id : number ;
email : string ;
@ Exclude ( )
password : string ;
}
Désormais, lorsque vous transformez un utilisateur, la propriété password
sera ignorée et ne sera pas incluse dans le résultat transformé.
Vous pouvez contrôler sur quelle opération vous excluez une propriété. Utilisez les options toClassOnly
ou toPlainOnly
:
import { Exclude } from 'class-transformer' ;
export class User {
id : number ;
email : string ;
@ Exclude ( { toPlainOnly : true } )
password : string ;
}
Désormais, la propriété password
sera exclue uniquement lors de l'opération instanceToPlain
. Vice versa, utilisez l'option toClassOnly
.
Vous pouvez ignorer toutes les propriétés de la classe et exposer uniquement celles qui sont explicitement nécessaires :
import { Exclude , Expose } from 'class-transformer' ;
@ Exclude ( )
export class User {
@ Expose ( )
id : number ;
@ Expose ( )
email : string ;
password : string ;
}
Désormais, id
et email
seront exposés et le mot de passe sera exclu lors de la transformation. Vous pouvez également définir une stratégie d'exclusion lors de la transformation :
import { instanceToPlain } from 'class-transformer' ;
let photo = instanceToPlain ( photo , { strategy : 'excludeAll' } ) ;
Dans ce cas, vous n'avez pas besoin de @Exclude()
une classe entière.
Si vous nommez vos propriétés privées avec un préfixe, disons avec _
, vous pouvez également exclure ces propriétés de la transformation :
import { instanceToPlain } from 'class-transformer' ;
let photo = instanceToPlain ( photo , { excludePrefixes : [ '_' ] } ) ;
Cela ignorera toutes les propriétés commençant par le préfixe _
. Vous pouvez transmettre n'importe quel nombre de préfixes et toutes les propriétés commençant par ces préfixes seront ignorées. Par exemple:
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" }
Vous pouvez utiliser des groupes pour contrôler quelles données seront exposées et lesquelles ne le seront pas :
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 vous créez une API comportant différentes versions, class-transformer dispose d'outils extrêmement utiles pour cela. Vous pouvez contrôler quelles propriétés de votre modèle doivent être exposées ou exclues dans quelle version. Exemple:
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
Parfois, vous avez une date dans votre objet javascript brut reçu au format chaîne. Et vous souhaitez créer un véritable objet Date javascript à partir de celui-ci. Vous pouvez le faire simplement en passant un objet Date au décorateur @Type
:
import { Type } from 'class-transformer' ;
export class User {
id : number ;
email : string ;
password : string ;
@ Type ( ( ) => Date )
registrationDate : Date ;
}
La même technique peut être utilisée avec les types primitifs Number
, String
, Boolean
lorsque vous souhaitez convertir vos valeurs dans ces types.
Lorsque vous utilisez des tableaux, vous devez fournir un type d'objet que contient le tableau. Ce type, vous le spécifiez dans un décorateur @Type()
:
import { Type } from 'class-transformer' ;
export class Photo {
id : number ;
name : string ;
@ Type ( ( ) => Album )
albums : Album [ ] ;
}
Vous pouvez également utiliser des types de tableaux personnalisés :
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 bibliothèque gérera automatiquement la transformation appropriée.
Les collections ES6 Set
et Map
nécessitent également le décorateur @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 > ;
}
Vous pouvez effectuer une transformation de données supplémentaire à l'aide du décorateur @Transform
. Par exemple, vous souhaitez faire de votre objet Date
un objet moment
lorsque vous transformez un objet simple en 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 ;
}
Désormais, lorsque vous appelez plainToInstance
et envoyez une représentation simple de l'objet Photo, il convertira une valeur de date dans votre objet photo en date du moment. @Transform
decorator prend également en charge les groupes et la gestion des versions.
Le décorateur @Transform
reçoit plus d'arguments pour vous permettre de configurer la manière dont vous souhaitez que la transformation soit effectuée.
@ Transform ( ( { value , key , obj , type } ) => value )
Argument | Description |
---|---|
value | La valeur de la propriété avant la transformation. |
key | Le nom de la propriété transformée. |
obj | L'objet source de transformation. |
type | Le type de transformation. |
options | L'objet options transmis à la méthode de transformation. |
Signature | Exemple | Description |
---|---|---|
@TransformClassToPlain | @TransformClassToPlain({ groups: ["user"] }) | Transformez le retour de la méthode avec instanceToPlain et exposez les propriétés sur la classe. |
@TransformClassToClass | @TransformClassToClass({ groups: ["user"] }) | Transformez le retour de la méthode avec instanceToInstance et exposez les propriétés sur la classe. |
@TransformPlainToClass | @TransformPlainToClass(User, { groups: ["user"] }) | Transformez le retour de la méthode avec plainToInstance et exposez les propriétés sur la classe. |
Les décorateurs ci-dessus acceptent un argument facultatif : ClassTransformOptions - Les options de transformation telles que les groupes, la version, le nom
Un exemple :
@ 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
ne contiendra que les propriétés firstName, lastName et email car ce sont les variables exposées. La propriété email est également exposée car nous avons mentionné le groupe "user.email".
Les génériques ne sont pas pris en charge car TypeScript n'a pas encore de bonnes capacités de réflexion. Une fois que l'équipe TypeScript nous fournira de meilleurs outils de réflexion de type d'exécution, les génériques seront implémentés. Cependant, vous pouvez utiliser quelques ajustements qui peuvent peut-être résoudre votre problème. Découvrez cet exemple.
REMARQUE Si vous utilisez class-validator avec class-transformer, vous NE souhaitez probablement PAS activer cette fonction.
Permet la conversion automatique entre les types intégrés en fonction des informations de type fournies par Typescript. Désactivé par défaut.
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
*/
Les références circulaires sont ignorées. Par exemple, si vous transformez la classe User
qui contient photos
de propriété avec le type Photo
et que Photo
contient un lien user
vers son User
parent, alors user
sera ignoré lors de la transformation. Les références circulaires ne sont pas ignorées uniquement lors de l’opération instanceToInstance
.
Disons que vous souhaitez télécharger des utilisateurs et que vous souhaitez qu'ils soient automatiquement mappés aux instances de la 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 ) ;
} ) ;
Vous pouvez également injecter une classe ClassTransformer
en tant que service dans providers
et utiliser ses méthodes.
Exemple d'utilisation avec angulaire 2 dans Plunker. Le code source est ici.
Jetez un œil aux exemples dans ./sample pour plus d’exemples d’utilisations.
Consultez les informations sur les dernières modifications et les notes de version ici.