项目地址: https://github.com/davanchen/easytype
欢迎使用EasyType:一个基于TypeScript的动态类型反射系统
众所周知JavaScript因为语言的特性,无法与JAVA一样提供一种动态类型反射机制,而市面上又缺乏完善的解决方案,EasyType的出现是为了从根本上解决这个问题, 赋予开发者尤其是后端开发者更多的能力。
警告:单元测试未完全覆盖,切勿用于商业项目。
开源只是为了交流技术、不想把一个好的理念埋没在个人手里,由于个人时间关系,本项目可能不会得到良好的维护,期望有成熟的公司或者团队能够改进或者重构它,让他成为node后端必备框架之一。
项目起源
从18年开始,我就决定让团队使用node+typescript来开发后端服务,经过一年多的实践,发现各种库都有一套自己的“建模语言”来申明类型,比如mongoose、数据验证器、GraphQL、GRPC、swagger等等。这不禁让我迷惑不解,为什么用了typescript以后还要重复写这么多的类型申明?于是我从19年初开始开发了这个框架陆续来实现。刚开始是通过AST来分析mongoose的schema生成模型接口和定义,但是没有从根本上解决问题,所以接下来是通过直接分析typescript的类申明来实现,后续又引入了json-schema标准,最后对枚举、方法、联合类型、泛型都提供了支持。
设计目标
- 能够覆盖typescript绝大多数的类型,尤其是对泛型能提供完善的支持。
- 尽可能的减少侵入,无需改动任何的代码
- 支持Transpile模式,无需构建即可直接运行,而且编译速度非常快
- 能够通过cli运行与构建项目,也能够脱离cli运行或者构建项目
方案对比
项目 | 对比 |
---|---|
io-ts | 定义了一套类型声明,设计目标可能主要是解决IO传输中的编码与解码 |
class-transformer | 定义了一套修饰器,只支持部分的TS类型 |
typescript-json-schema | 需要调用CLI生成JSON格式的类型声明,非动态 |
tsruntime | 是和typescript-json-schema一样在TypeCheck阶段实现,因此不支持Transpile模式 |
type-reflect | 类似,但功能不够完善 |
使用场景
引入EasyType将为你的后端开发带来更大的想象空间,其中我们团队用到的部分就包括:
- 不用添加任何代码,将TS类申明直接转换成mongoose schema
- 不用添加任何代码,可以直接使用各种json-schema数据验证器
- 不用添加任何代码,动态生成API文档,稍微改动就能生成OpenAPI规范的swagger文档,你的接口改动团队其他成员可以随时看到。
- 不用添加任何代码,动态生成RPC声明,比如一键生成GRPC proto申明文件,把微服务开发变成一件很轻松的事情。
- 由于后端能够输出完整的类型申明,因此前端(尤其是后台开发)能够快速的构建出数据显示、操作界面,会使开发变得更快速高效。
原理
通过typescript的自定义transform,在编译阶段把类型写入类描述中,最后在运行时生成json-schema标准的类型说明。
@Reflectable()
export class User extends Document {
/** 用户ID */
uid: number;
/** 用户名 */
username: string;
}
比如以上代码,通过编译器将变为:
export class User extends Document {
$easy.IsObject({
$type: 1,
$properties: {
uid: {
$type: 2,
$description: "\u7528\u6237ID",
$ref: Number
},
username: {
$type: 2,
$description: "\u7528\u6237\u540D",
$ref: String
},
},
$target: User,
$id: "User",
$extends: mongoose_1.Document
})
private $metadata: any;
}
通过在运行时调用 Schema.getMetadata(User), 即可得到User的类型声明(json-schema):
{
"type": "object",
"properties": {
"_id": {
"type": "string",
"format": "OBJECT_ID",
"description": "ID"
},
"uid": {
"type": "number",
"description": "用户ID",
},
"username": {
"type": "string",
"description": "用户名",
}
},
"required": [
"_id",
"uid",
"username"
],
"$id": "User",
"$x": "Document"
}
更强大的ENUM
在typescript中enum的存在更像是为了描述类型(你在运行时很难分清哪个是key,哪个是value),这也就不能与JAVA一样提供一些操作, 因此EasyType在编译阶段增加一些方法,使之变得更加灵活和强大,借助这些特性我们就能够实现无缝输出ENUM信息到API文档,而无需再书写任何的注释或者代码。
export interface EnumInterface<T> {
readonly keys: string[];
readonly values: number[] | string[];
getValue(key: string): Undefinable<T>;
hasValue(value: any): boolean;
getKeys(value: any): string[];
getKey(value: any): Undefinable<string>;
hasKey(key: string): boolean;
getDescription(key: string): Undefinable<string>;
}
export type Enum<T = any, V = number | string> =
{ readonly [P in keyof T]: T[P]; }
& Readonly<EnumInfo>
& EnumInterface<V>
;
现在你可以通过 Enum<Foo>.Keys 和 Enum<Bar>.values 获得键值,也可以拿到对应的像这样的类型申明:
{
"name": "AssetType",
"description": "用户资产类型",
"fields": [
{
"key": "BALANCE",
"value": 1,
"description": "账户余额"
},
{
"key": "POINTS",
"value": 3,
"description": "账户积分"
}
]
},
部分继承
使用关键词extends会继承基类所有的属性,有时候如果你想部分继承基类属性,可以使用Inherits语法糖:
@Reflectable()
export class UserLoginDto implements Inherits<User> {
username: string;
password: string;
}
方法反射
方法的注释、修饰符、参数、返回值等信息会被标注到Metadata中,可以通过 Reflect.getMetadata(‘easy:metadata’, target, propertyKey) 获取。
还有哪些问题?
- 还没来得及做完整的单元测试,所以暂时不能用于商业项目
- 泛型目前编译器这块已完成,但是运行时由于时间关系还未能提供支持
- 由于设计原因,只能支持类的输出,不支持interface和type的输出,因为前者在js运行时以function存在,后者不存在于运行时,或许后面会想办法支持。
- 低版本的TypeScript(3.7以下)会有一些问题,所以用最新的TSC编译吧。
插播一条广告
即将推出基于EasyType+nest.js的全家桶开发包,尽情期待。 项目地址: https://github.com/davanchen/easynest
演示:借助vscode插件(即将开源)一键生成proto
演示:EasyNest API文档模块自动生成API描述(枚举、控制器、模型)
不错的东东。 TS如果能支持对类型名称的转换(比如下划线转驼峰)很多功能就好做了。 我这儿有个支持 interface 的类似轮子 kmore 大家可参考。
@waitingsong 我们设计的出发点不同,其实也可以在EasyType之上实现kmore
@davanchen 我是基于 knex 简单包装下方便调用。你这个通用型好。
不错
为啥不用Typegoose?Nest+Typegoose+TypeGraphQL 就完事了啊,定义一个 model 一个 dto 解决所有问题
不错
期待EasyNest可以尽早开源
顶
@andyhu 我能用一次定义解决的事情为啥要定义2次?如果整个项目还有其他的套件,就会有更多的类型定义存在。model和dto本身就有关联性,通过对model的继承或者部分继承就可以避免重复定义dto.
比如: export class UserLoginDto implements Inherits<User> { username: string; password: string; }
export class CreateAddrDto implements Omits<Addr,’_id’> {
}
export class ModifyAddrDto extends Addr {
}