Es ist die ES6- und Typescript-Ära. Heutzutage arbeitet man mehr denn je mit Klassen und Konstruktorobjekten. Mit dem Klassentransformator können Sie ein einfaches Objekt in eine Instanz einer Klasse umwandeln und umgekehrt. Außerdem ermöglicht es die Serialisierung/Deserialisierung von Objekten basierend auf Kriterien. Dieses Tool ist sowohl im Frontend als auch im Backend sehr nützlich.
Beispiel für die Verwendung mit Winkel 2 im Plunker. Der Quellcode ist hier verfügbar.
In JavaScript gibt es zwei Arten von Objekten:
Einfache Objekte sind Objekte, die Instanzen der Object
-Klasse sind. Manchmal werden sie Literalobjekte genannt, wenn sie mit der {}
-Notation erstellt werden. Klassenobjekte sind Instanzen von Klassen mit eigenen definierten Konstruktoren, Eigenschaften und Methoden. Normalerweise definieren Sie sie über class
.
Also, was ist das Problem?
Manchmal möchten Sie ein einfaches Javascript-Objekt in die vorhandenen ES6- Klassen umwandeln. Wenn Sie beispielsweise einen JSON-Code aus Ihrem Backend, einer API oder einer JSON-Datei laden und nach dem JSON.parse
Parsen ein einfaches Javascript-Objekt haben, keine Instanz Ihrer Klasse.
Sie haben beispielsweise eine Liste von Benutzern in Ihrer users.json
, die Sie laden:
[
{
"id" : 1 ,
"firstName" : " Johny " ,
"lastName" : " Cage " ,
"age" : 27
},
{
"id" : 2 ,
"firstName" : " Ismoil " ,
"lastName" : " Somoni " ,
"age" : 50
},
{
"id" : 3 ,
"firstName" : " Luke " ,
"lastName" : " Dacascos " ,
"age" : 12
}
]
Und Sie haben eine 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 ;
}
}
Sie gehen davon aus, dass Sie Benutzer vom Typ User
aus der Datei users.json
herunterladen und möglicherweise den folgenden Code schreiben möchten:
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
} ) ;
In diesem Code können Sie users[0].id
und auch users[0].firstName
und users[0].lastName
verwenden. Allerdings können Sie users[0].getName()
oder users[0].isAdult()
nicht verwenden, da „users“ eigentlich ein Array aus einfachen Javascript-Objekten und keine Instanzen des User-Objekts ist. Sie haben den Compiler tatsächlich angelogen, als Sie sagten, dass seine users: User[]
.
Was also tun? Wie erstelle ich ein users
aus Instanzen von User
anstelle von einfachen Javascript-Objekten? Die Lösung besteht darin, neue Instanzen des Benutzerobjekts zu erstellen und alle Eigenschaften manuell in neue Objekte zu kopieren. Aber sobald Sie eine komplexere Objekthierarchie haben, können die Dinge sehr schnell schief gehen.
Alternativen? Ja, Sie können den Klassentransformator verwenden. Der Zweck dieser Bibliothek besteht darin, Ihnen dabei zu helfen, Ihre einfachen Javascript-Objekte den Instanzen Ihrer Klassen zuzuordnen.
Diese Bibliothek eignet sich auch hervorragend für Modelle, die in Ihren APIs verfügbar gemacht werden, da sie ein hervorragendes Tool zur Steuerung dessen bietet, was Ihre Modelle in Ihrer API verfügbar machen. Hier ist ein Beispiel, wie es aussehen wird:
fetch ( 'users.json' ) . then ( ( users : Object [ ] ) => {
const realUsers = plainToInstance ( User , users ) ;
// now each user in realUsers is an instance of User class
} ) ;
Jetzt können Sie die Methoden users[0].getName()
und users[0].isAdult()
verwenden.
Modul installieren:
npm install class-transformer --save
reflect-metadata
Shim ist erforderlich, installieren Sie es ebenfalls:
npm install reflect-metadata --save
und stellen Sie sicher, dass Sie es an einem globalen Ort importieren, z. B. app.ts:
import 'reflect-metadata' ;
Es werden ES6-Funktionen verwendet. Wenn Sie eine alte Version von node.js verwenden, müssen Sie möglicherweise es6-shim installieren:
npm install es6-shim --save
und importieren Sie es an einem globalen Ort wie app.ts:
import 'es6-shim' ;
Modul installieren:
npm install class-transformer --save
reflect-metadata
Shim ist erforderlich, installieren Sie es ebenfalls:
npm install reflect-metadata --save
Fügen Sie <script>
zu „reflect-metadata“ im Kopf Ihrer index.html
hinzu:
< html >
< head >
<!-- ... -->
< script src =" node_modules/reflect-metadata/Reflect.js " > </ script >
</ head >
<!-- ... -->
</ html >
Wenn Sie Winkel 2 verwenden, sollte diese Unterlegscheibe bereits installiert sein.
Wenn Sie system.js verwenden, möchten Sie dies möglicherweise zur map
und package
hinzufügen:
{
"map" : {
"class-transformer" : " node_modules/class-transformer "
},
"packages" : {
"class-transformer" : { "main" : " index.js " , "defaultExtension" : " js " }
}
}
Diese Methode wandelt ein einfaches Javascript-Objekt in eine Instanz einer bestimmten Klasse um.
import { plainToInstance } from 'class-transformer' ;
let users = plainToInstance ( User , userJson ) ; // to convert user plain object a single user. also supports arrays
Diese Methode wandelt ein einfaches Objekt in eine Instanz um, indem sie ein bereits gefülltes Objekt verwendet, das eine Instanz der Zielklasse ist.
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.
Diese Methode wandelt Ihr Klassenobjekt wieder in ein einfaches Javascript-Objekt um, das später JSON.stringify
sein kann.
import { instanceToPlain } from 'class-transformer' ;
let photo = instanceToPlain ( photo ) ;
Diese Methode wandelt Ihr Klassenobjekt in eine neue Instanz des Klassenobjekts um. Dies kann als tiefer Klon Ihrer Objekte behandelt werden.
import { instanceToInstance } from 'class-transformer' ;
let photo = instanceToInstance ( photo ) ;
Sie können in den Transformationsoptionen auch die Option ignoreDecorators
verwenden, um alle von Ihren Klassen verwendeten Dekoratoren zu ignorieren.
Mit der serialize
können Sie Ihr Modell direkt in JSON serialisieren:
import { serialize } from 'class-transformer' ;
let photo = serialize ( photo ) ;
serialize
funktioniert sowohl mit Arrays als auch mit Nicht-Arrays.
Sie können Ihr Modell mithilfe der deserialize
-Methode von JSON aus deserialisieren:
import { deserialize } from 'class-transformer' ;
let photo = deserialize ( Photo , photo ) ;
Damit die Deserialisierung mit Arrays funktioniert, verwenden Sie die Methode deserializeArray
:
import { deserializeArray } from 'class-transformer' ;
let photos = deserializeArray ( Photo , photos ) ;
Das Standardverhalten der Methode plainToInstance
besteht darin, alle Eigenschaften des einfachen Objekts festzulegen, auch diejenigen, die nicht in der Klasse angegeben sind.
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',
// }
Wenn dieses Verhalten Ihren Anforderungen nicht entspricht, können Sie die Option excludeExtraneousValues
in der Methode plainToInstance
verwenden und gleichzeitig alle Ihre Klasseneigenschaften als Anforderung verfügbar machen .
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'
// }
Wenn Sie versuchen, Objekte mit verschachtelten Objekten zu transformieren, müssen Sie wissen, welchen Objekttyp Sie transformieren möchten. Da Typescript noch nicht über gute Reflexionsfähigkeiten verfügt, sollten wir implizit angeben, welchen Objekttyp jede Eigenschaft enthält. Dies geschieht mit @Type
-Decorator.
Nehmen wir an, wir haben ein Album mit Fotos. Und wir versuchen, das einfache Albumobjekt in ein Klassenobjekt umzuwandeln:
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
Falls das verschachtelte Objekt unterschiedlichen Typs sein kann, können Sie ein zusätzliches Optionsobjekt bereitstellen, das einen Diskriminator angibt. Die Option „Diskriminator“ muss eine property
definieren, die den Subtypnamen für das Objekt und die möglichen subTypes
enthält, in die das verschachtelte Objekt konvertiert werden kann. Ein Untertyp hat einen value
, der den Konstruktor des Typs enthält, und den name
, der mit der property
des Diskriminators übereinstimmen kann.
Nehmen wir an, wir haben ein Album mit einem Top-Foto. Aber dieses Foto kann von ganz unterschiedlicher Art sein. Und wir versuchen, ein einfaches Albumobjekt in ein Klassenobjekt umzuwandeln. Die einfache Objekteingabe muss die zusätzliche Eigenschaft __type
definieren. Diese Eigenschaft wird während der Transformation standardmäßig entfernt:
JSON-Eingabe :
{
"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.
Hinweis: Gleiches gilt für Arrays mit unterschiedlichen Untertypen. Darüber hinaus können Sie in den Optionen keepDiscriminatorProperty: true
angeben, um die Diskriminatoreigenschaft auch innerhalb Ihrer resultierenden Klasse beizubehalten.
Sie können offenlegen, was Ihr Getter oder Ihre Methode zurückgibt, indem Sie einen @Expose()
Dekorator für diese Getter oder Methoden festlegen:
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 ;
}
}
Wenn Sie einige der Eigenschaften mit einem anderen Namen verfügbar machen möchten, können Sie dies tun, indem Sie eine name
für @Expose
Dekorator angeben:
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 ;
}
}
Manchmal möchten Sie einige Eigenschaften während der Transformation überspringen. Dies kann mit dem @Exclude
Dekorator erfolgen:
import { Exclude } from 'class-transformer' ;
export class User {
id : number ;
email : string ;
@ Exclude ( )
password : string ;
}
Wenn Sie nun einen Benutzer transformieren, wird die Eigenschaft password
übersprungen und nicht in das transformierte Ergebnis einbezogen.
Sie können steuern, bei welchem Vorgang Sie eine Eigenschaft ausschließen. Verwenden Sie die Optionen toClassOnly
oder toPlainOnly
:
import { Exclude } from 'class-transformer' ;
export class User {
id : number ;
email : string ;
@ Exclude ( { toPlainOnly : true } )
password : string ;
}
Jetzt wird password
nur während instanceToPlain
-Vorgangs ausgeschlossen. Umgekehrt verwenden Sie die Option toClassOnly
.
Sie können alle Eigenschaften der Klasse überspringen und nur diejenigen offenlegen, die explizit benötigt werden:
import { Exclude , Expose } from 'class-transformer' ;
@ Exclude ( )
export class User {
@ Expose ( )
id : number ;
@ Expose ( )
email : string ;
password : string ;
}
Jetzt werden id
und email
offengelegt und das Passwort wird während der Transformation ausgeschlossen. Alternativ können Sie während der Transformation eine Ausschlussstrategie festlegen:
import { instanceToPlain } from 'class-transformer' ;
let photo = instanceToPlain ( photo , { strategy : 'excludeAll' } ) ;
In diesem Fall müssen Sie nicht eine ganze Klasse @Exclude()
.
Wenn Sie Ihre privaten Eigenschaften mit einem Präfix benennen, beispielsweise mit _
, dann können Sie auch solche Eigenschaften von der Transformation ausschließen:
import { instanceToPlain } from 'class-transformer' ;
let photo = instanceToPlain ( photo , { excludePrefixes : [ '_' ] } ) ;
Dadurch werden alle Eigenschaften übersprungen, die mit dem Präfix _
beginnen. Sie können eine beliebige Anzahl von Präfixen übergeben und alle Eigenschaften, die mit diesen Präfixen beginnen, werden ignoriert. Zum Beispiel:
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" }
Mithilfe von Gruppen können Sie steuern, welche Daten offengelegt werden und welche nicht:
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
Wenn Sie eine API mit verschiedenen Versionen erstellen, verfügt Class-Transformer über äußerst nützliche Tools dafür. Sie können steuern, welche Eigenschaften Ihres Modells in welcher Version verfügbar gemacht oder ausgeschlossen werden sollen. Beispiel:
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
Manchmal wird in Ihrem einfachen Javascript-Objekt ein Datum in einem Zeichenfolgenformat empfangen. Und Sie möchten daraus ein echtes Javascript-Datumsobjekt erstellen. Sie können dies einfach tun, indem Sie ein Date-Objekt an den @Type
Dekorator übergeben:
import { Type } from 'class-transformer' ;
export class User {
id : number ;
email : string ;
password : string ;
@ Type ( ( ) => Date )
registrationDate : Date ;
}
Die gleiche Technik kann mit den primitiven Typen Number
, String
und Boolean
verwendet werden, wenn Sie Ihre Werte in diese Typen konvertieren möchten.
Wenn Sie Arrays verwenden, müssen Sie einen Typ des Objekts angeben, das das Array enthält. Diesen Typ geben Sie in einem @Type()
-Dekorator an:
import { Type } from 'class-transformer' ;
export class Photo {
id : number ;
name : string ;
@ Type ( ( ) => Album )
albums : Album [ ] ;
}
Sie können auch benutzerdefinierte Array-Typen verwenden:
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 ;
}
Die Bibliothek führt die ordnungsgemäße Transformation automatisch durch.
Für die ES6-Sammlungen Set
und „ Map
ist außerdem der @Type
Dekorator erforderlich:
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 > ;
}
Sie können zusätzliche Datentransformationen mit @Transform
Dekorator durchführen. Sie möchten beispielsweise Ihr Date
zu einem moment
machen, wenn Sie ein Objekt von „einfach“ in „klassisch“ umwandeln:
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 ;
}
Wenn Sie nun plainToInstance
aufrufen und eine einfache Darstellung des Fotoobjekts senden, wird ein Datumswert in Ihrem Fotoobjekt in ein Momentdatum umgewandelt. @Transform
Decorator unterstützt auch Gruppen und Versionierung.
Der @Transform
Dekorator erhält weitere Argumente, mit denen Sie konfigurieren können, wie die Transformation durchgeführt werden soll.
@ Transform ( ( { value , key , obj , type } ) => value )
Argument | Beschreibung |
---|---|
value | Der Eigenschaftswert vor der Transformation. |
key | Der Name der umgewandelten Eigenschaft. |
obj | Das Transformationsquellobjekt. |
type | Der Transformationstyp. |
options | Das an die Transformationsmethode übergebene Optionsobjekt. |
Unterschrift | Beispiel | Beschreibung |
---|---|---|
@TransformClassToPlain | @TransformClassToPlain({ groups: ["user"] }) | Transformieren Sie die Methodenrückgabe mit „instanceToPlain“ und machen Sie die Eigenschaften der Klasse verfügbar. |
@TransformClassToClass | @TransformClassToClass({ groups: ["user"] }) | Transformieren Sie die Methodenrückgabe mit „instanceToInstance“ und machen Sie die Eigenschaften der Klasse verfügbar. |
@TransformPlainToClass | @TransformPlainToClass(User, { groups: ["user"] }) | Transformieren Sie die Methodenrückgabe mit plainToInstance und machen Sie die Eigenschaften der Klasse verfügbar. |
Die oben genannten Dekoratoren akzeptieren ein optionales Argument: ClassTransformOptions – Die Transformationsoptionen wie Gruppen, Version, Name
Ein Beispiel:
@ 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 ( ) ;
Die user
enthält nur die Eigenschaften „Vorname“, „Nachname“ und „E-Mail“, da es sich dabei um die offengelegten Variablen handelt. Die E-Mail-Eigenschaft wird ebenfalls angezeigt, da wir die Gruppe „user.email“ erwähnt haben.
Generics werden nicht unterstützt, da TypeScript noch nicht über gute Reflexionsfähigkeiten verfügt. Sobald das TypeScript-Team uns bessere Tools zur Laufzeittypreflexion zur Verfügung stellt, werden Generika implementiert. Es gibt jedoch einige Optimierungen, die Sie verwenden können, um Ihr Problem möglicherweise zu lösen. Schauen Sie sich dieses Beispiel an.
HINWEIS Wenn Sie den Klassenvalidator zusammen mit dem Klassentransformator verwenden, möchten Sie diese Funktion wahrscheinlich NICHT aktivieren.
Ermöglicht die automatische Konvertierung zwischen integrierten Typen basierend auf den von Typescript bereitgestellten Typinformationen. Standardmäßig deaktiviert.
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
*/
Zirkelverweise werden ignoriert. Wenn Sie beispielsweise die Klasse User
transformieren, die photos
vom Typ Photo
enthält, und Photo
einen Link user
zu seinem übergeordneten Element User
enthält, wird user
während der Transformation ignoriert. Zirkelverweise werden nicht nur während instanceToInstance
-Operation ignoriert.
Nehmen wir an, Sie möchten Benutzer herunterladen und möchten, dass sie automatisch den Instanzen der User
zugeordnet werden.
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 ) ;
} ) ;
Sie können eine Klasse ClassTransformer
auch als Dienst in providers
einfügen und deren Methoden verwenden.
Beispiel für die Verwendung mit Winkel 2 im Plunker. Der Quellcode ist hier.
Weitere Anwendungsbeispiele finden Sie in den Beispielen unter ./sample.
Informationen zu wichtigen Änderungen und Versionshinweise finden Sie hier.