Это эпоха ES6 и Typescript. Сегодня вы работаете с классами и объектами-конструкторами больше, чем когда-либо. Класс-трансформер позволяет преобразовать простой объект в некоторый экземпляр класса и наоборот. Также он позволяет сериализовать/десериализовать объект на основе критериев. Этот инструмент очень полезен как для внешнего, так и для внутреннего интерфейса.
Пример использования с angular 2 в plunker. Исходный код доступен здесь.
В JavaScript существует два типа объектов:
Обычные объекты — это объекты, которые являются экземплярами класса Object
. Иногда их называют литеральными объектами, если они созданы с помощью нотации {}
. Объекты класса — это экземпляры классов с собственным определенным конструктором, свойствами и методами. Обычно вы определяете их через нотацию class
.
Итак, в чем проблема?
Иногда вам нужно преобразовать простой объект JavaScript в имеющиеся у вас классы ES6. Например, если вы загружаете json из своего бэкэнда, какого-либо API или из файла json, и после того, как вы JSON.parse
его, у вас есть простой объект javascript, а не экземпляр имеющегося у вас класса.
Например, у вас есть список пользователей в users.json
, который вы загружаете:
[
{
"id" : 1 ,
"firstName" : " Johny " ,
"lastName" : " Cage " ,
"age" : 27
},
{
"id" : 2 ,
"firstName" : " Ismoil " ,
"lastName" : " Somoni " ,
"age" : 50
},
{
"id" : 3 ,
"firstName" : " Luke " ,
"lastName" : " Dacascos " ,
"age" : 12
}
]
И у вас есть класс 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 ;
}
}
Вы предполагаете, что загружаете пользователей типа User
из users.json
и можете написать следующий код:
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
} ) ;
В этом коде вы можете использовать users[0].id
, вы также можете использовать users[0].firstName
и users[0].lastName
. Однако вы не можете использовать users[0].getName()
или users[0].isAdult()
потому что «пользователи» на самом деле представляют собой массив простых объектов javascript, а не экземпляры объекта User. На самом деле вы солгали компилятору, когда сказали, что его users: User[]
.
Так что же делать? Как создать массив users
из экземпляров объектов User
вместо простых объектов JavaScript? Решение состоит в том, чтобы создать новые экземпляры объекта «Пользователь» и вручную скопировать все свойства в новые объекты. Но если у вас более сложная иерархия объектов, все может пойти не так очень быстро.
Альтернативы? Да, вы можете использовать преобразователь классов. Цель этой библиотеки — помочь вам сопоставить простые объекты JavaScript с экземплярами имеющихся у вас классов.
Эта библиотека также отлично подходит для моделей, представленных в ваших API, поскольку она предоставляет отличный инструмент для управления тем, что ваши модели предоставляют в вашем API. Вот пример, как это будет выглядеть:
fetch ( 'users.json' ) . then ( ( users : Object [ ] ) => {
const realUsers = plainToInstance ( User , users ) ;
// now each user in realUsers is an instance of User class
} ) ;
Теперь вы можете использовать методы users[0].getName()
и users[0].isAdult()
.
Установить модуль:
npm install class-transformer --save
Требуется прокладка reflect-metadata
, установите ее тоже:
npm install reflect-metadata --save
и обязательно импортируйте его в глобальное место, например app.ts:
import 'reflect-metadata' ;
Используются функции ES6. Если вы используете старую версию node.js, вам может потребоваться установить es6-shim:
npm install es6-shim --save
и импортируйте его в глобальное место, например app.ts:
import 'es6-shim' ;
Установить модуль:
npm install class-transformer --save
Требуется прокладка reflect-metadata
, установите ее тоже:
npm install reflect-metadata --save
добавьте <script>
для отражения метаданных в заголовке вашего index.html
:
< html >
< head >
<!-- ... -->
< script src =" node_modules/reflect-metadata/Reflect.js " > </ script >
</ head >
<!-- ... -->
</ html >
Если вы используете angular 2, у вас уже должна быть установлена эта прокладка.
Если вы используете system.js, вы можете добавить это в конфигурацию map
и package
:
{
"map" : {
"class-transformer" : " node_modules/class-transformer "
},
"packages" : {
"class-transformer" : { "main" : " index.js " , "defaultExtension" : " js " }
}
}
Этот метод преобразует простой объект JavaScript в экземпляр определенного класса.
import { plainToInstance } from 'class-transformer' ;
let users = plainToInstance ( User , userJson ) ; // to convert user plain object a single user. also supports arrays
Этот метод преобразует простой объект в экземпляр, используя уже заполненный объект, который является экземпляром целевого класса.
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.
Этот метод преобразует ваш объект класса обратно в простой объект javascript, который позже может быть JSON.stringify
.
import { instanceToPlain } from 'class-transformer' ;
let photo = instanceToPlain ( photo ) ;
Этот метод преобразует ваш объект класса в новый экземпляр объекта класса. Это можно рассматривать как глубокое клонирование ваших объектов.
import { instanceToInstance } from 'class-transformer' ;
let photo = instanceToInstance ( photo ) ;
Вы также можете использовать опцию ignoreDecorators
в параметрах преобразования, чтобы игнорировать все декораторы, которые используют ваши классы.
Вы можете сериализовать свою модель прямо в json, используя метод serialize
:
import { serialize } from 'class-transformer' ;
let photo = serialize ( photo ) ;
serialize
работает как с массивами, так и с не-массивами.
Вы можете десериализовать свою модель из json, используя метод deserialize
:
import { deserialize } from 'class-transformer' ;
let photo = deserialize ( Photo , photo ) ;
Чтобы десериализация работала с массивами, используйте метод deserializeArray
:
import { deserializeArray } from 'class-transformer' ;
let photos = deserializeArray ( Photo , photos ) ;
Поведение метода plainToInstance
по умолчанию — установить все свойства простого объекта, даже те, которые не указаны в классе.
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',
// }
Если такое поведение не соответствует вашим потребностям, вы можете использовать опцию excludeExtraneousValues
в методе plainToInstance
, предоставляя при этом все свойства вашего класса в качестве требования.
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'
// }
Когда вы пытаетесь преобразовать объекты, имеющие вложенные объекты, необходимо знать, какой тип объекта вы пытаетесь преобразовать. Поскольку Typescript пока не обладает хорошими возможностями отражения, мы должны неявно указать, какой тип объекта содержит каждое свойство. Это делается с помощью декоратора @Type
.
Допустим, у нас есть альбом с фотографиями. И мы пытаемся преобразовать простой объект альбома в объект класса:
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
Если вложенный объект может иметь разные типы, вы можете предоставить дополнительный объект параметров, который указывает дискриминатор. Опция дискриминатора должна определять property
, содержащее имя подтипа объекта и возможные subTypes
, в которые может быть преобразован вложенный объект. Подтип имеет value
, содержащее конструктор типа, и name
, которое может совпадать со property
дискриминатора.
Допустим, у нас есть альбом с верхней фотографией. Но эта фотография может быть совершенно разной. И мы пытаемся преобразовать простой объект альбома в объект класса. Входные данные простого объекта должны определять дополнительное свойство __type
. Это свойство по умолчанию удаляется во время преобразования:
Ввод 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.
Подсказка: то же самое относится и к массивам с разными подтипами. Более того, вы можете указать keepDiscriminatorProperty: true
в параметрах, чтобы сохранить свойство дискриминатора также внутри результирующего класса.
Вы можете раскрыть то, что возвращает ваш геттер или метод, установив декоратор @Expose()
для этих геттеров или методов:
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 ;
}
}
Если вы хотите предоставить некоторым свойствам другое имя, вы можете сделать это, указав параметр name
для декоратора @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 ;
}
}
Иногда вам хочется пропустить некоторые свойства во время преобразования. Это можно сделать с помощью декоратора @Exclude
:
import { Exclude } from 'class-transformer' ;
export class User {
id : number ;
email : string ;
@ Exclude ( )
password : string ;
}
Теперь при преобразовании пользователя свойство password
будет пропущено и не будет включено в преобразованный результат.
Вы можете указать, при какой операции вы исключите свойство. Используйте параметры toClassOnly
или toPlainOnly
:
import { Exclude } from 'class-transformer' ;
export class User {
id : number ;
email : string ;
@ Exclude ( { toPlainOnly : true } )
password : string ;
}
Теперь свойство password
будет исключено только во время операции instanceToPlain
. И наоборот, используйте опцию toClassOnly
.
Вы можете пропустить все свойства класса и явно указать только те, которые необходимы:
import { Exclude , Expose } from 'class-transformer' ;
@ Exclude ( )
export class User {
@ Expose ( )
id : number ;
@ Expose ( )
email : string ;
password : string ;
}
Теперь id
и email
будут раскрыты, а пароль будет исключен во время преобразования. Альтернативно вы можете установить стратегию исключения во время преобразования:
import { instanceToPlain } from 'class-transformer' ;
let photo = instanceToPlain ( photo , { strategy : 'excludeAll' } ) ;
В этом случае вам не нужно @Exclude()
целый класс.
Если вы называете свои частные свойства префиксом, скажем, _
, то вы также можете исключить такие свойства из преобразования:
import { instanceToPlain } from 'class-transformer' ;
let photo = instanceToPlain ( photo , { excludePrefixes : [ '_' ] } ) ;
При этом будут пропущены все свойства, начинающиеся с префикса _
. Вы можете передать любое количество префиксов, и все свойства, начинающиеся с этих префиксов, будут игнорироваться. Например:
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" }
Вы можете использовать группы, чтобы контролировать, какие данные будут доступны, а какие нет:
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
Если вы создаете API, имеющий разные версии, у class-transformer есть для этого чрезвычайно полезные инструменты. Вы можете контролировать, какие свойства вашей модели в какой версии следует раскрыть или исключить. Пример:
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
Иногда у вас есть дата в вашем простом объекте javascript, полученная в строковом формате. И вы хотите создать из него настоящий объект Date javascript. Вы можете сделать это, просто передав объект Date декоратору @Type
:
import { Type } from 'class-transformer' ;
export class User {
id : number ;
email : string ;
password : string ;
@ Type ( ( ) => Date )
registrationDate : Date ;
}
Тот же метод можно использовать с примитивными типами Number
, String
, Boolean
когда вы хотите преобразовать свои значения в эти типы.
При использовании массивов необходимо указать тип объекта, который содержит массив. Этот тип вы указываете в декораторе @Type()
:
import { Type } from 'class-transformer' ;
export class Photo {
id : number ;
name : string ;
@ Type ( ( ) => Album )
albums : Album [ ] ;
}
Вы также можете использовать собственные типы массивов:
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 ;
}
Библиотека автоматически выполнит правильное преобразование.
Для коллекций Set
и Map
ES6 также требуется декоратор @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 > ;
}
Вы можете выполнить дополнительное преобразование данных с помощью декоратора @Transform
. Например, вы хотите, чтобы ваш объект Date
был объектом moment
, когда вы преобразуете объект из простого в класс:
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 ;
}
Теперь, когда вы вызываете plainToInstance
и отправляете простое представление объекта Photo, оно преобразует значение даты в вашем объекте фотографии в дату момента. Декоратор @Transform
также поддерживает группы и управление версиями.
Декоратору @Transform
предоставляется больше аргументов, позволяющих настроить способ выполнения преобразования.
@ Transform ( ( { value , key , obj , type } ) => value )
Аргумент | Описание |
---|---|
value | Значение свойства до преобразования. |
key | Имя преобразованного свойства. |
obj | Исходный объект преобразования. |
type | Тип трансформации. |
options | Объект параметров передается методу преобразования. |
Подпись | Пример | Описание |
---|---|---|
@TransformClassToPlain | @TransformClassToPlain({ groups: ["user"] }) | Преобразуйте возвращаемый метод с помощью экземпляраToPlain и раскройте свойства класса. |
@TransformClassToClass | @TransformClassToClass({ groups: ["user"] }) | Преобразуйте возвращаемый метод с помощью экземпляраToInstance и раскройте свойства класса. |
@TransformPlainToClass | @TransformPlainToClass(User, { groups: ["user"] }) | Преобразуйте возвращаемый метод с помощью PlainToInstance и раскройте свойства класса. |
Вышеупомянутые декораторы принимают один необязательный аргумент: ClassTransformOptions — параметры преобразования, такие как группы, версия, имя.
Пример:
@ 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 ( ) ;
переменная user
будет содержать только свойства firstName,lastName, email, поскольку они являются открытыми переменными. Свойство электронной почты также доступно, поскольку мы упомянули группу «user.email».
Обобщенные шаблоны не поддерживаются, поскольку TypeScript пока не обладает хорошими возможностями отражения. Как только команда TypeScript предоставит нам лучшие инструменты отражения типов во время выполнения, дженерики будут реализованы. Однако вы можете использовать некоторые настройки, которые, возможно, помогут решить вашу проблему. Посмотрите этот пример.
ПРИМЕЧАНИЕ. Если вы используете класс-валидатор вместе с класс-трансформером, вы, вероятно, НЕ хотите включать эту функцию.
Включает автоматическое преобразование между встроенными типами на основе информации о типе, предоставленной Typescript. По умолчанию отключено.
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
*/
Циклические ссылки игнорируются. Например, если вы преобразуете класс User
, который содержит photos
свойств с типом Photo
, а Photo
содержит ссылку user
на родительского User
, то user
будет игнорироваться во время преобразования. Циклические ссылки не игнорируются только во время операции instanceToInstance
.
Допустим, вы хотите загрузить пользователей и автоматически сопоставить их с экземплярами класса 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 ) ;
} ) ;
Вы также можете внедрить класс ClassTransformer
как сервис в providers
и использовать его методы.
Пример использования с angular 2 в plunker. Исходный код здесь.
Посмотрите примеры в ./sample, чтобы увидеть больше примеров использования.
Информацию о критических изменениях и примечаниях к выпуску можно найти здесь.