类转换器的作用是将普通的javascript对象转换成类对象。我们通过api端点或者json文件访问所得的是普通的json文本,一般我们通过JSON.parse把其转换成普通的javascript对象,但是有时候我们想让它变成一个类的对象而不是普通的javascript对象。比如用class-validator来验证从后端api获取的json字符串时,我们就需要自动把json转为待验证类的对象而不是一个js对象。

例如我们现在可以读取远程api的一个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.json来获取User的对象数组

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[]对象数组了

fetch("users.json").then((users: Object[]) => {
    const realUsers = plainToClass(User, users);
    // now each user in realUsers is instance of User class
});

安装

  1. 安装class-transformer:
    npm install class-transformer --save
  2. 安装reflect-metadata

reflect-metadata是必须的,具体使用请看我这边文章)

​ 安装后在app.ts这种顶层文件你需要import "reflect-metadata";

基础方法

plainToClass

普通对象转换为类对象

import {plainToClass} from "class-transformer";

let users = plainToClass(User, userJson); // to convert user plain object a single user. also supports arrays

plainToClassFromExist

普通对象合并已经创建的类实例

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文本

import {classToPlain} from "class-transformer";
let photo = classToPlain(photo);

classToClass

克隆类实例

import {classToClass} from "class-transformer";
let photo = classToClass(photo);

可以使用ignoreDecorators选项去除所有原实例中的装饰器

serialize

直接把类实例转换为json文本,是不是数组都可以转换

import {serialize} from "class-transformer";
let photo = serialize(photo);

deserialize 和 deserializeArray

直接把json文本转换为类对象

import {deserialize} from "class-transformer";
let photo = deserialize(Photo, photo);

如果json文本是个对象数组请使用deserializeArray方法

import {deserializeArray} from "class-transformer";
let photos = deserializeArray(Photo, photos);

强制类型安全

plainToClass会把所有的被转换对象的属性全部类实例的属性,即时类中并不存在某些属性

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装饰器来指定需要公开的属性

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装饰器来隐式地指定属性所属的类

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相同的一个字段,并把值设置为需要转换的子类型的名称。判断器还需要指定所有的子类型值以及其名称,具体示例如下

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和方法的返回值

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选项来实现:

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属性将会在classToPlain操作中排除,相反的可以使用toClassOnly

跳过类的所有属性

你可以通过在类上添加@Exclude装饰器并且在需要公开的属性上添加@Expose装饰器来只公开指定的属性

import {Exclude, Expose} from "class-transformer";

@Exclude()
export class User {

    @Expose()
    id: number;

    @Expose()
    email: string;

    password: string;
}

另外,您可以在转换期间设置排除策略:

import {classToPlain} from "class-transformer";
let photo = classToPlain(photo, { strategy: "excludeAll" });

这时你不需要在添加@Exclude装饰器了

跳过私有属性或某些前缀属性

我们可以排除公开具有指定前缀的属性以及私有属性

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" }

使用组来控制排除的属性

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具有非常有用的工具。您可以控制应在哪个版本中公开或排除模型的哪些属性。示例

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将会被转回为字符串

import {Type} from "class-transformer";

export class User {

    id: number;

    email: string;

    password: string;

    @Type(() => Date)
    registrationDate: Date;
}

当您想将值转换为Number, String, Boolean 类型时也是这样做

数组处理

当你想转换数组时,你必须使用@Type装饰器指定数组项的类型也可以使用自定义的数组类型

SetMap也是一样

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库的对象:

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,示例:

@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提供的类型信息,启用内置类型之间的自动转换。默认禁用。

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操作。

Last modification:February 25th, 2020 at 01:19 am
原创不易,请赞助一包烟钱,让lichnow能更好地服务您^v^