Project Icon

class-transformer

TypeScript对象转换与序列化工具库

class-transformer是一个功能丰富的TypeScript库,专门用于对象转换和序列化。它支持将普通JavaScript对象转换为类实例,也可以进行反向操作。该库提供了多种实用方法,如plainToInstance和instanceToPlain,可以处理嵌套对象、暴露getter和方法返回值,以及选择性地跳过属性。class-transformer在处理API数据和复杂对象结构时尤其有效,适用于前端和后端开发。

class-transformer

构建状态 代码覆盖率 npm版本

这是ES6和TypeScript的时代。如今,您比以往任何时候都更多地使用类和构造函数对象。 Class-transformer允许您将普通对象转换为某个类的实例,反之亦然。 它还允许基于特定条件序列化/反序列化对象。 这个工具在前端和后端都非常有用。

plunker中可以查看如何与Angular 2一起使用的示例。 源代码可在这里找到。

目录

什么是class-transformer

在JavaScript中有两种类型的对象:

  • 普通(字面量)对象
  • 类(构造函数)对象

普通对象是Object类的实例。 有时它们被称为字面量对象,通过{}符号创建。 类对象是具有自己定义的构造函数、属性和方法的类的实例。 通常通过class符号定义它们。

那么,问题是什么?

有时您想将普通JavaScript对象转换为您拥有的ES6。 例如,如果您正在从后端、某个API或JSON文件加载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;
  }
}

假设您正在从users.json文件下载User类型的用户,您可能想编写以下代码:

fetch('users.json').then((users: User[]) => {
  // 您可以在这里使用users,类型提示也会对您可用,
  // 但users实际上并不是User类的实例
  // 这意味着您不能使用User类的方法
});

在这段代码中,您可以使用users[0].id,也可以使用users[0].firstNameusers[0].lastName。 但是您不能使用users[0].getName()users[0].isAdult(),因为"users"实际上是 普通JavaScript对象的数组,而不是User对象的实例。 当您说它是users: User[]时,您实际上欺骗了编译器。

那么该怎么办呢?如何让users数组成为User对象的实例而不是普通JavaScript对象? 解决方案是创建User对象的新实例,并手动将所有属性复制到新对象中。 但是一旦您有更复杂的对象层次结构,事情可能很快就会出错。

有替代方案吗?是的,您可以使用class-transformer。这个库的目的是帮助您将普通JavaScript 对象映射到您拥有的类的实例。

这个库对于在API中暴露的模型也很棒, 因为它提供了很好的工具来控制您的模型在API中暴露的内容。 以下是它看起来的样子:

fetch('users.json').then((users: Object[]) => {
  const realUsers = plainToInstance(User, users);
  // 现在realUsers中的每个用户都是User类的实例
});

现在您可以使用users[0].getName()users[0].isAdult()方法了。

安装

Node.js

  1. 安装模块:

    npm install class-transformer --save

  2. 需要reflect-metadata shim,也安装它:

    npm install reflect-metadata --save

    并确保在全局位置导入它,比如app.ts:

    import 'reflect-metadata';
    
  3. 使用了ES6特性,如果您使用的是旧版本的node.js,可能需要安装es6-shim:

    npm install es6-shim --save

    并在全局位置导入它,比如app.ts:

import 'es6-shim';

浏览器

  1. 安装模块:

    npm install class-transformer --save

  2. 需要安装 reflect-metadata shim:

    npm install reflect-metadata --save

    index.html 的 head 中添加 reflect-metadata 的 <script> 标签:

    <html>
      <head>
        <!-- ... -->
        <script src="node_modules/reflect-metadata/Reflect.js"></script>
      </head>
      <!-- ... -->
    </html>
    

    如果你使用的是 Angular 2,应该已经安装了这个 shim。

  3. 如果你使用 system.js,你可能想在 mappackage 配置中添加以下内容:

    {
      "map": {
        "class-transformer": "node_modules/class-transformer"
      },
      "packages": {
        "class-transformer": { "main": "index.js", "defaultExtension": "js" }
      }
    }
    

方法

plainToInstance

此方法将普通的 JavaScript 对象转换为特定类的实例。

import { plainToInstance } from 'class-transformer';

let users = plainToInstance(User, userJson); // 将用户普通对象转换为单个用户实例。也支持数组

plainToClassFromExist

此方法将普通对象转换为实例,使用已填充的目标类实例对象。

const defaultUser = new User();
defaultUser.role = 'user';

let mixedUser = plainToClassFromExist(defaultUser, user); // 当没有设置值时,mixedUser 应具有 role = user 的值,否则使用原值。

instanceToPlain

此方法将类对象转换回普通的 JavaScript 对象,之后可以使用 JSON.stringify

import { instanceToPlain } from 'class-transformer';
let photo = instanceToPlain(photo);

instanceToInstance

此方法将类对象转换为该类对象的新实例。 这可以被视为对象的深度克隆。

import { instanceToInstance } from 'class-transformer';
let photo = instanceToInstance(photo);

你还可以在转换选项中使用 ignoreDecorators 选项来忽略类中使用的所有装饰器。

serialize

你可以使用 serialize 方法直接将模型序列化为 JSON:

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

serialize 适用于数组和非数组。

deserialize 和 deserializeArray

你可以使用 deserialize 方法从 JSON 反序列化模型:

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',
// }

如果这种行为不适合你的需求,你可以在 plainToInstance 方法中使用 excludeExtraneousValues 选项, 同时需要将所有类属性标记为公开。

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);
// 现在 album 是一个 Album 对象,其中包含 Photo 对象

提供多个类型选项

如果嵌套对象可以是不同类型,你可以提供一个额外的选项对象, 该对象指定一个鉴别器。鉴别器选项必须定义一个 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;
}

让 album 等于使用 plainToInstance 将 albumJson 转换为 Album 实例。 // 现在 album 是一个 Album 对象,其中包含一个没有 __type 属性的 UnderWater 对象。


提示:这同样适用于具有不同子类型的数组。此外,你可以在选项中指定 `keepDiscriminatorProperty: true` 以在结果类中也保留判别器属性。

## 暴露 getter 和方法返回值

你可以通过在 getter 或方法上设置 `@Expose()` 装饰器来暴露它们的返回值:

```typescript
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;
}

现在当你转换 User 时,password 属性将被跳过,不会包含在转换结果中。

根据操作跳过

你可以控制在哪种操作中排除属性。使用 toClassOnlytoPlainOnly 选项:

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

现在 idemail 将被暴露,而 password 在转换过程中将被排除。 或者,你可以在转换过程中设置排除策略:

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: ['_'] });
// 这里 plainUser 将等于
// { id: 1, name: "Johny Cage" }

使用分组控制排除的属性

你可以使用分组来控制哪些数据将被暴露,哪些不会:

import { Exclude, Expose, instanceToPlain } from 'class-transformer';

export class User {
  id: number;

  name: string;

  @Expose({ groups: ['user', 'admin'] }) // 这意味着这些数据只会暴露给用户和管理员
  email: string;

  @Expose({ groups: ['user'] }) // 这意味着这些数据只会暴露给用户
  password: string;
}

let user1 = instanceToPlain(user, { groups: ['user'] }); // 将包含 id、name、email 和 password
let user2 = instanceToPlain(user, { groups: ['admin'] }); // 将包含 id、name 和 email

使用版本控制来控制暴露和排除的属性

如果你正在构建一个有不同版本的 API,class-transformer 为此提供了非常有用的工具。 你可以控制在哪个版本中应该暴露或排除模型的哪些属性。例如:

import { Exclude, Expose, instanceToPlain } from 'class-transformer';

export class User {
  id: number;

  name: string;

  @Expose({ since: 0.7, until: 1 }) // 这意味着这个属性将从版本 0.7 开始暴露,直到版本 1
  email: string;

  @Expose({ since: 2.1 }) // 这意味着这个属性将从版本 2.1 开始暴露
  password: string;
}

let user1 = instanceToPlain(user, { version: 0.5 }); // 将包含 id 和 name
let user2 = instanceToPlain(user, { version: 0.7 }); // 将包含 id、name 和 email
let user3 = instanceToPlain(user, { version: 1 }); // 将包含 id 和 name
let user4 = instanceToPlain(user, { version: 2 }); // 将包含 id 和 name
let user5 = instanceToPlain(user, { version: 2.1 }); // 将包含 id、name 和 password

将日期字符串转换为 Date 对象

有时你的普通 JavaScript 对象中会以字符串格式接收到一个日期。 而你想从中创建一个真正的 JavaScript Date 对象。 你可以简单地通过将 Date 对象传递给 @Type 装饰器来实现:

import { Type } from 'class-transformer';

export class User {
  id: number;

  email: string;

  password: string;

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

当你想将值转换为这些类型时,同样的技术也可以用于 NumberStringBoolean 等基本类型。

使用数组

当使用数组时,你必须提供数组包含的对象类型。 这个类型需要在 @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> {
  // 自定义数组函数 ...
}

export class Photo {
  id: number;

  name: string;

  @Type(() => Album)
  albums: AlbumCollection;
}

库会自动处理适当的转换。

ES6 集合 SetMap 也需要 @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 对象的普通表示时, 它会将你的 photo 对象中的日期值转换为 moment 日期。 @Transform 装饰器还支持分组和版本控制。

高级用法

@Transform 装饰器提供了更多参数,让你可以配置如何进行转换。

@Transform(({ value, key, obj, type }) => value)
参数描述
value转换前的属性值
key被转换属性的名称
obj转换源对象
type转换类型
options传递给转换方法的选项对象

其他装饰器

签名示例描述
@TransformClassToPlain@TransformClassToPlain({ groups: ["user"] })使用 instanceToPlain 转换方法返回值,并在类上暴露属性
@TransformClassToClass@TransformClassToClass({ groups: ["user"] })使用 instanceToInstance 转换方法返回值,并在类上暴露属性
@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 属性,因为它们是暴露的变量。 email 属性也被暴露,因为我们提到了 "user.email" 分组。

处理泛型

由于 TypeScript 目前还没有很好的反射能力,泛型暂不支持。 一旦 TypeScript 团队为我们提供更好的运行时类型反射工具,泛型将会被实现。 不过,你可以使用一些技巧,也许能解决你的问题。 查看这个示例。

隐式类型转换

注意 如果你同时使用 class-validator 和 class-transformer,你可能不想启用此功能。

根据 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 将是 `{ prop: "1234" }` - 注意 prop 值已被转换为字符串。
 *  result2 将是 `{ prop: 1234 }` - 默认行为
 */

如何处理循环引用?

循环引用会被忽略。 例如,如果你正在转换包含 photos 属性(类型为 Photo)的 User 类, 而 Photo 包含指向其父 Useruser 链接,那么在转换过程中 user 将被忽略。 循环引用只在 instanceToInstance 操作中不被忽略。

Angular2 示例

假设你想下载用户并希望它们自动映射到 User 类的实例。

import { plainToInstance } from 'class-transformer';

this.http
  .get('users.json')
  .map(res => res.json())
  .map(res => plainToInstance(User, res as Object[]))
  .subscribe(users => {
    // 现在 "users" 的类型是 User[],每个用户都可以使用 getName() 和 isAdult() 方法
    console.log(users);
  });

你也可以在 providers 中注入 ClassTransformer 类作为服务,并使用其方法。

plunker 中查看如何与 Angular 2 一起使用的示例。 源代码在这里

示例

查看 ./sample 中的示例,了解更多使用示例。

发布说明

有关重大变更和发布说明的信息,请参阅此处

项目侧边栏1项目侧边栏2
推荐项目
Project Cover

豆包MarsCode

豆包 MarsCode 是一款革命性的编程助手,通过AI技术提供代码补全、单测生成、代码解释和智能问答等功能,支持100+编程语言,与主流编辑器无缝集成,显著提升开发效率和代码质量。

Project Cover

AI写歌

Suno AI是一个革命性的AI音乐创作平台,能在短短30秒内帮助用户创作出一首完整的歌曲。无论是寻找创作灵感还是需要快速制作音乐,Suno AI都是音乐爱好者和专业人士的理想选择。

Project Cover

有言AI

有言平台提供一站式AIGC视频创作解决方案,通过智能技术简化视频制作流程。无论是企业宣传还是个人分享,有言都能帮助用户快速、轻松地制作出专业级别的视频内容。

Project Cover

Kimi

Kimi AI助手提供多语言对话支持,能够阅读和理解用户上传的文件内容,解析网页信息,并结合搜索结果为用户提供详尽的答案。无论是日常咨询还是专业问题,Kimi都能以友好、专业的方式提供帮助。

Project Cover

阿里绘蛙

绘蛙是阿里巴巴集团推出的革命性AI电商营销平台。利用尖端人工智能技术,为商家提供一键生成商品图和营销文案的服务,显著提升内容创作效率和营销效果。适用于淘宝、天猫等电商平台,让商品第一时间被种草。

Project Cover

吐司

探索Tensor.Art平台的独特AI模型,免费访问各种图像生成与AI训练工具,从Stable Diffusion等基础模型开始,轻松实现创新图像生成。体验前沿的AI技术,推动个人和企业的创新发展。

Project Cover

SubCat字幕猫

SubCat字幕猫APP是一款创新的视频播放器,它将改变您观看视频的方式!SubCat结合了先进的人工智能技术,为您提供即时视频字幕翻译,无论是本地视频还是网络流媒体,让您轻松享受各种语言的内容。

Project Cover

美间AI

美间AI创意设计平台,利用前沿AI技术,为设计师和营销人员提供一站式设计解决方案。从智能海报到3D效果图,再到文案生成,美间让创意设计更简单、更高效。

Project Cover

稿定AI

稿定设计 是一个多功能的在线设计和创意平台,提供广泛的设计工具和资源,以满足不同用户的需求。从专业的图形设计师到普通用户,无论是进行图片处理、智能抠图、H5页面制作还是视频剪辑,稿定设计都能提供简单、高效的解决方案。该平台以其用户友好的界面和强大的功能集合,帮助用户轻松实现创意设计。

投诉举报邮箱: service@vectorlightyear.com
@2024 懂AI·鲁ICP备2024100362号-6·鲁公网安备37021002001498号