出处:英文原文
类转换器的作用是将普通的javascript对象转换成类对象。我们通过api端点或者json文件访问所得的是普通的json文本,一般我们通过JSON.parse把其转换成普通的javascript对象,但是有时候我们想让它变成一个类的对象而不是普通的javascript对象。比如用class-validator来验证从后端api获取的json字符串时,我们就需要自动把json转为待验证类的对象而不是一个js对象。
例如我们现在可以读取远程api的一个users.json
的内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 [{ "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
类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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.json
来获取User
的对象数组
1 2 3 4 5 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].firstname但是由于你获取的是普通的js对象而非User类的对象,所以你无法调用users[0].getName()方法,而class-transformer就是为了把普通的js对象按你的需求转换成类对象而生的。
你只要像下面这样就可以创建真正的User[]对象数组了
1 2 3 4 fetch("users.json").then((users: Object[]) => { const realUsers = plainToClass(User, users); // now each user in realUsers is instance of User class });
安装 安装class-transformer
:npm install class-transformer --save
安装reflect-metadata
:
安装后在app.ts这种顶层文件你需要import "reflect-metadata";
基础方法 plainToClass 普通对象转换为类对象
1 2 3 import {plainToClass} from "class-transformer"; let users = plainToClass(User, userJson); // to convert user plain object a single user. also supports arrays
plainToClassFromExist 普通对象合并已经创建的类实例
1 2 3 4 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.
classToPlain 类实例转换为普通对象
转换后可以使用JSON.stringify再转成普通的json文本
1 2 import {classToPlain} from "class-transformer"; let photo = classToPlain(photo);
classToClass 克隆类实例
1 2 import {classToClass} from "class-transformer"; let photo = classToClass(photo);
可以使用ignoreDecorators选项去除所有原实例中的装饰器
serialize 直接把类实例转换为json文本,是不是数组都可以转换
1 2 import {serialize} from "class-transformer"; let photo = serialize(photo);
deserialize 和 deserializeArray 直接把json文本转换为类对象
1 2 import {deserialize} from "class-transformer"; let photo = deserialize(Photo, photo);
如果json文本是个对象数组请使用deserializeArray方法
1 2 import {deserializeArray} from "class-transformer"; let photos = deserializeArray(Photo, photos);
强制类型安全 plainToClass会把所有的被转换对象的属性全部类实例的属性,即时类中并不存在某些属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import {plainToClass} from "class-transformer"; class User { id: number firstName: string lastName: string } const fromPlainUser = { unkownProp: 'hello there', firstName: 'Umed', lastName: 'Khudoiberdiev', } console.log(plainToClass(User, fromPlainUser)) // User { // unkownProp: 'hello there', // firstName: 'Umed', // lastName: 'Khudoiberdiev', // }
你可以使用excludeExtraneousValues选项结合Expose装饰器来指定需要公开的属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import {Expose, plainToClass} 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(plainToClass(User, fromPlainUser, { excludeExtraneousValues: true })) // User { // id: undefined, // firstName: 'Umed', // lastName: 'Khudoiberdiev' // }
子类型转换 嵌套对象 由于现在Typescript对反射还没有非常好的支持,所以你需要使用@Type装饰器来隐式地指定属性所属的类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import {Type, plainToClass} from "class-transformer"; export class Album { id: number; name: string; @Type(() => Photo) photos: Photo[]; } export class Photo { id: number; filename: string; } let album = plainToClass(Album, albumJson); // now album is Album object with Photo objects inside
多类型选项 一个嵌套的子类型也可以匹配多个类型,这可以通过判断器实现。判断器需要指定一个 property,而被转换js对象中的嵌套对象的也必须拥有与property相同的一个字段,并把值设置为需要转换的子类型的名称。判断器还需要指定所有的子类型值以及其名称,具体示例如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 import {Type, plainToClass} from "class-transformer"; const albumJson = { "id": 1, "name": "foo", "topPhoto": { "id": 9, "filename": "cool_wale.jpg", "depth": 1245, "__type": "underwater" } } 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 = plainToClass(Album, albumJson); // now album is Album object with a UnderWater object without `__type` property.
此外可以设置keepDiscriminatorProperty: true,这样可以把判断器的属性也包含在转换后的对象中
排除与公开 公开方法的返回值 添加@Expose装饰器即可公开getter和方法的返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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; } }
公开属性为不同名称 如果要使用其他名称公开某些属性,可以通过为@Expose装饰器指定name选项来实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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装饰器完成:
1 2 3 4 5 6 7 8 9 10 11 import {Exclude} from "class-transformer"; export class User { id: number; email: string; @Exclude() password: string; }
现在,当您转换用户时,password属性将被跳过,并且不包含在转换结果中。
根据操作决定跳过 我们可以通过toClassOnly或者toPlainOnly来控制一个属性在哪些操作中需要排除
1 2 3 4 5 6 7 8 9 10 11 import {Exclude} from "class-transformer"; export class User { id: number; email: string; @Exclude({ toPlainOnly: true }) password: string; }
现在password属性将会在classToPlain操作中排除,相反的可以使用toClassOnly
跳过类的所有属性 你可以通过在类上添加@Exclude装饰器并且在需要公开的属性上添加@Expose装饰器来只公开指定的属性
1 2 3 4 5 6 7 8 9 10 11 12 13 import {Exclude, Expose} from "class-transformer"; @Exclude() export class User { @Expose() id: number; @Expose() email: string; password: string; }
另外,您可以在转换期间设置排除策略:
1 2 import {classToPlain} from "class-transformer"; let photo = classToPlain(photo, { strategy: "excludeAll" });
这时你不需要在添加@Exclude装饰器了
跳过私有属性或某些前缀属性 我们可以排除公开具有指定前缀的属性以及私有属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import {Expose} 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 = classToPlain(user, { excludePrefixes: ["_"] }); // here plainUser will be equal to // { id: 1, name: "Johny Cage" }
使用组来控制排除的属性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import {Exclude, Expose} from "class-transformer"; @Exclude() 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 = classToPlain(user, { groups: ["user"] }); // will contain id, name, email and password let user2 = classToPlain(user, { groups: ["admin"] }); // will contain id, name and email
使用版本范围来控制公开和排除的属性 如果要构建具有不同版本的API,则class-transformer具有非常有用的工具。您可以控制应在哪个版本中公开或排除模型的哪些属性。示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import {Exclude, Expose} from "class-transformer"; @Exclude() 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 = classToPlain(user, { version: 0.5 }); // will contain id and name let user2 = classToPlain(user, { version: 0.7 }); // will contain id, name and email let user3 = classToPlain(user, { version: 1 }); // will contain id and name let user4 = classToPlain(user, { version: 2 }); // will contain id and name let user5 = classToPlain(user, { version: 2.1 }); // will contain id, name nad password
特殊处理 将日期字符串转换为Date对象 有时,您的JavaScript对象中有一个以字符串格式接收的Date。您想从中创建一个真正的javascript Date对象。您只需将Date对象传递给@Type装饰器即可完成此操作:
当从类对象反向转换为普通对象时registrationDate将会被转回为字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 import {Type} from "class-transformer"; export class User { id: number; email: string; password: string; @Type(() => Date) registrationDate: Date; }
当您想将值转换为Number, String, Boolean 类型时也是这样做
数组处理 当你想转换数组时,你必须使用@Type装饰器指定数组项的类型也可以使用自定义的数组类型
Set和Map也是一样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import {Type} from "class-transformer"; export class AlbumCollection extends Array<Album> { // custom array functions ... } export class Photo { id: number; name: string; @Type(() => Album) albums: Album[]; // albums: AlbumCollection; 使用自定义类型 } 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库的对象:
1 2 3 4 5 6 7 8 9 10 11 12 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; }
现在当执行plainToClass转换后的对象中的date属性将是一个Moment对象。@Transform同样支持组和版本。
高级用法 @Transform有更多的参数给你创建自定义的转换逻辑
@Transform((value, obj, type) => value) 参数 描述 value 自定义转换执行前的属性值 obj 转换源对象 type 转换的类型
其他装饰器 签名 示例 @TransformClassToPlain @TransformClassToPlain({ groups: [“user”] }) @TransformClassToClass @TransformClassToClass({ groups: [“user”] }) @TransformPlainToClas @TransformPlainToClass(User, { groups: [“user”] }) 上述装饰器接受一个可选参数:ClassTransformOptions-转换选项,例如groups, version, name,示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @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,latstname和email
使用泛型 由于目前Typescript对反射的支持还没有完善,所以只能使用其它替代方案,具体可以查看这个例子
隐式类型转换 你如果将class-validator与class-transformer一起使用,则可能不想启用此功能。
根据Typescript提供的类型信息,启用内置类型之间的自动转换。默认禁用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { IsString } from 'class-validator' class MyPayload { @IsString() prop: string } const result1 = plainToClass(MyPayload, { prop: 1234 }, { enableImplicitConversion: true }); const result2 = plainToClass(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包含一个Photo类型的photos数组属性,而Photo又包含一个属性链接到User,则转换过程中此属性会被忽略,除了classToClass操作。