nestjs关于class-transformer的实践

出处:英文原文

类转换器的作用是将普通的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操作。