Skip to content

FiredRice/class-formatter

Repository files navigation

class-formatter 使用文档

目录

简介

一套装饰器风格的数据格式化方法。

根据模板对数据进行格式化,返回的数据结构一定符合模板定义类型。

使用场景

确保数据类型

在日常开发过程中想必大家都遇到过类似下面的错误:

Uncaught TypeError: Cannot read properties of undefined (reading 'map')

数据在各模块间传递时,经常会因为某一环节的失误而导致数据类型发生变化,从而导致因类型错误而出现的 bug。 class-formatter 可以确保数据类型的准确,从而尽可能过滤掉因类型错误导致的 bug。

复杂数据结构格式化

在一些复杂场景中,会遇到层层嵌套的复杂数据结构,而为了确保每条数据的准确性常常要编写复杂的数据格式化方法。 class-formatter 可以通过装饰器简化这种格式化过程,减轻心智负担。

安装

yarn add class-formatter

注意:

该版本为兼容 typescript 5 以上的 stage3 提案的装饰器,需对tsconfig.json做出如下配置:

{
    "compilerOptions": {
        ...
        "experimentalDecorators": false,
        "emitDecoratorMetadata": false,
        ...
    },
    ...
}

若您的项目不能进行如上配置,请将版本切换到小于 5.0.0,参考文档

名称释义

  • 模板:TransModel 装饰器修饰的类即是模板。
  • 指令: 模板中属性或方法的 class-formatter 装饰器,一个装饰器即为一个指令。
  • 源数据: 被转换的数据。
  • 转换: 调用 executeTransformexecuteTransArray 函数对源数据进行格式化的行为。

例如:

@TransModel
class User {
    @toString()
    name!: string;

    @toNumber()
    age!: number;
}

const user = {
    name: '张三',
    age: 18
};

const result = executeTransform(User, user);
  • 模板:上述示例中,类 User 即为模板,也是转换函数(executeTransform)的第一个参数。
  • 指令:上述示例中,@toString()@toNumber() 即为指令。
  • 源数据:上述示例中,对象 user 即为源数据。

使用方法

普通对象

@TransModel
class User {
    @toString()
    name!: string;

    @toNumber()
    age!: number;
}

const user = {
    name: '张三',
    age: '18'
};

const formatUser = executeTransform(User, user);
// => { name: '张三', age: 18 }

在上述示例中,formatUser 一定拥有 字符串类型 属性 name数字类型 属性 age

数组

@TransModel
class User {
    @toString()
    name!: string;

    @toNumber()
    age!: number;
}

const users = [{
    name: '张三',
    age: '18'
}];

const formatUsers = executeTransArray(User, users);

若源数据为数组,则可以使用 executeTransArray 进行格式化。

默认值

@TransModel
class User {
    @toString()
    name: string = '张三';

    @toNumber(1)
    age!: number;
}
  • 若源数据中不存在属性,则会根据默认值生成该属性。
    • 例如根据上述模板,若源数据不存在 name 属性,则转换结果一定拥有字符串属性 name,且 name 属性值为 '张三'。
  • 默认值有两种传入方式,模板传入与指令传入,其中模板传入优先级 高于 指令传入。
    • 上述模板中,name 为模板传入,age 为指令传入。

API

属性装饰器 说明 类型 默认值
toNumber 若属性为非数字类型,则将属性转换为 number 类型。
autoTranstrue 时会自动将字符串转换为数字。
(value?: NumberConfig | number) => ClassFieldDecorator defaultValue: 0
autoTrans: true
toString 若属性为非字符串类型,则将属性转换为 string 类型。
autoTranstrue 时会自动将数字转换为字符串。
(value?: StringConfig | string) => ClassFieldDecorator defaultValue: ''
autoTrans: true
toBoolean 若属性为非布尔类型,则将属性转换为 boolean 类型 (value?: BooleanConfig | boolean) => ClassFieldDecorator defaultValue: false
toSymbol 若属性为非 symbol 类型,则将属性转换为 symbol 类型 (value?: SymbolConfig | symbol) => ClassFieldDecorator defaultValue: Symbol()
toRegExp 若属性为非正则类型,则将属性转换为正则类型 (value?: RegConfig | RegExp | string) => ClassFieldDecorator defaultValue: new RegExp('')
toType 若属性为非对象类型,则将属性转换为对象。
若指定了 Type,则可以将类型转换为 Type 的类型。
(value?: ObjectConfig | Type) => ClassFieldDecorator defaultValue: {}
toArray 若属性为非数组类型,则将属性转换为数组。
若指定了 Type,则可以将数组内所有数据转换为 Type 的类型。
(value?: ArrayConfig | Type) => ClassFieldDecorator defaultValue: []
toKeep 保持源数据引用 keys?: ModelKey | ModelKey[]) => ClassFieldDecorator --
Remove 移除属性。若传入回调函数且回调函数返回 true ,则删除该属性,否则删除 (value?: RemoveConfig | RemoveCallback | ModelKey | ModelKey[]) => ClassFieldAndMethodDecorator --
Format 对属性进行自定义格式化。
注意:Format 不限制返回值类型,使用时请格外注意
(callback: FormatCallback, keys?: ModelKey | ModelKey[]) => ClassFieldDecorator --
Rename 对属性重命名。 (name: string, keys?: ModelKey | ModelKey[]) => ClassFieldAndMethodDecorator --
方法装饰器 说明 类型 默认值
Extend 在结果中继承被装饰的方法或访问器 (keys?: ModelKey | ModelKey[]) => ClassMethodDecorator --
Remove 移除方法。注意:需先使用 Extend 继承方法 (value?: RemoveConfig | RemoveCallback | ModelKey | ModelKey[]) => ClassFieldAndMethodDecorator --
Rename 重命名方法。注意:需先使用 Extend 继承方法 (name: string, keys?: ModelKey | ModelKey[]) => ClassFieldAndMethodDecorator --
类装饰器 说明 类型 默认值
TransModel 声名类为模板 ClassDecorator --
Extend 继承父类的全部装饰器 (parent: Type) => ClassDecorator --
Mixins 混入,同时继承全部类的装饰器 (...parents: Type[]) => ClassDecorator --

executeTransform

参数名称 说明 类型
ClassType 模板类 Type
values 被格式化对象 any
options 配置项 Omit<FormatOptions, 'map'>

executeTransArray

参数名称 说明 类型
ClassType 模板类 Type
values 被格式化对象 any[]
options 配置项 FormatOptions

Interface

Decorator

type Decorator = (value, context: DecoratorContext) => void;

ClassDecorator

type ClassDecorator = (value: Function, context: ClassDecoratorContext) => void;

ClassFieldDecorator

type ClassFieldDecorator = (value: undefined, context: ClassFieldDecoratorContext) => void;

ClassFieldAndMethodDecorator

type ClassFieldAndMethodDecorator = (value: undefined | Function, context: ClassMethodDecoratorContext | ClassFieldDecoratorContext) => void;

ClassMethodDecorator

type ClassMethodDecorator = (value: Function, context: ClassMethodDecoratorContext | ClassGetterDecoratorContext | ClassSetterDecoratorContext) => void;

NumberConfig

type NumberConfig = { 
    defaultValue?: number;
    autoTrans?: boolean;
    keys?: ModelKey | ModelKey[];
};

StringConfig

type StringConfig = { 
    defaultValue?: string;
    autoTrans?: boolean;
    keys?: ModelKey | ModelKey[];
};

BooleanConfig

type BooleanConfig = { 
    defaultValue?: boolean;
    keys?: ModelKey | ModelKey[]; 
};

SymbolConfig

type SymbolConfig = { 
    defaultValue?: symbol;
    keys?: ModelKey | ModelKey[]; 
};

RegConfig

type RegConfig = { 
    defaultValue?: Regexp | string;
    keys?: ModelKey | ModelKey[]; 
};

ObjectConfig

type ObjectConfig<T = any> = { 
    defaultValue?: Partial<T>; 
    ClassType?: Type<T>;
    keys?: ModelKey | ModelKey[];
};

ArrayConfig

type ArrayConfig<T = any> = { 
    defaultValue?: Partial<T>[]; 
    ClassType?: Type<T>;
    keys?: ModelKey | ModelKey[];
    map?: (value: T, index: number, array: T[]) => T;
};

FormatCallback

type FormatCallback = (item, values) => any;
名称 说明
item 属性被转换后的值
values 源数据
注意:values 源数据的直接引用,请勿在转换过程中对其进行修改
shareValue 共享数据
注意:shareValue 为共享数据的直接引用,请勿在转换过程中对其进行修改

RemoveCallback

type RemoveCallback = (value: any, target: Readonly<any>, shareValue?: any) => boolean;

RemoveConfig

type RemoveConfig = {
    beforeRemove?: RemoveCallback;
    keys?: ModelKey | ModelKey[];
};

Type

interface Type<T = any> extends Function {
    new(...args: any[]): T;
}

ModelKey
执行键类型

type ModelKey = string | number;

FormatOptions

type FormatOptions<T> = {
    mergeSource?: boolean;
    key?: ModelKey;
    shareValue?: any;
    deep?: number;
    map?: (value: T, index: number, array: T[]) => T;
}
名称 说明 类型
mergeSource 是否将 源数据 合并到转换结果中 boolean
key 执行键 ModelKey
shareValue 共享数据。可在自定义装饰器与 Format 中获取的额外数据 any
deep 转换深度限制。详情 boolean
map 原生数组的 map 方法。仅在 executeTransArray 中生效 (value: T, index: number, array: T[]) => T

自定义属性装饰器

const CustomDecorator = createFormatDecorator((values, shareValue, ...args) => {
    // ...Do something
    return values.name;
});

// type CustomeDecorator = (...args) => DecoratorFun

@TransModel
class User {
    @CustomDecorator('Hello')
    name!: string;
}

createFormatDecorator

属性 说明 类型
callback 装饰器执行回调 (values, shareValue, ...args) => any
keys 可选参数 执行键 ModelKey | ModelKey[]

callback

属性 说明 类型
values 源数据
注意:values 为源数据的直接引用,请勿在转换过程中对其进行修改
any
shareValue 共享数据
注意:shareValue 为共享数据的直接引用,请勿在转换过程中对其进行修改
any
args 在生成的装饰器中传入的参数 any[]

关于执行键

executeTransform 的 options 属性中提供了 key 属性,以下称为 rootKey 。 在所有指令中均提供了 keys 属性的入口,以下称为 propertyKeys 。

  • 若 propertyKeys 不存在,则指令会被无条件执行。
  • 若 propertyKeys 存在,则当且仅当 propertyKeys 中 包含 rootKey 时该指令才会执行。
class User {
    @toString({ keys: 'submit' })
    name!: string;

    @toNumber()
    age!: number;
}

const user = {
    name: '张三',
    age: '18'
};

const formatUser = executeTransform(User, user, {
    key: 'submit'
});

上述示例中:

  • toNumber 指令会无条件执行。
  • executeTransform 中传入的 key 为 'submit',则 toString 指令会被执行,否则将忽略 name 属性。

由于执行键的存在,我们可以方便的在同一个模板上定制多套格式化方法。如下:

class User {
    @toString()
    @toString({ defaultValue: '张三', keys: '1' })
    @toString({ defaultValue: '李四', keys: '2' })
    @toString({ defaultValue: '王五', keys: '3' })
    name!: string;

    @toNumber()
    age!: number;
}

注意:当一个属性拥有多个装饰器时,模板的可读性下降,且难以迭代。因此建议优先考虑模板继承策略进行个性化复用。如无必要请尽量避免使用执行键。

class-formatter 提供了 createBatchDecorators 方法用于对多装饰器进行管理。

createBatchDecorators

属性 说明 类型
...decorators 需要统一管理的装饰器 PropertyDecorator[]

通过 createBatchDecorators 方法,我们可以对上述案例进行管理:

const NameManage = createBatchDecorators(
    toString({ defaultValue: '张三', keys: '1' }),
    toString({ defaultValue: '李四', keys: '2' }),
    toString({ defaultValue: '王五', keys: '3' })
);

@TransModel
class User {
    @toString()
    @NameManage()
    name!: string;

    @toNumber()
    age!: number;
}

如上将所有拥有执行键的装饰器封装成 NameManage 装饰器,toString 作为默认格式化指令,NameManage 则根据执行键分发指令。

关于循环引用

  • 若多个模板间存在循环引用,则子模板中引用的父模板失效( typescript 自身限制)。
  • 若模板自身循环引用,则默认可转化深度为 50,超过该深度的转换会被忽略。
  • 若被转换对象存在循环引用,则忽略循环属性。

模板自循环

@TransModel
class Person {
    @toString()
    name!: string;

    @toType(Person)
    child!: Person;
}

const target = {
    name: '父亲',
    child: {
        name: 1
    }
}

// target.child 会被忽略
executeTransform(Person, {});

模板自循环理论上允许存在,但可能导致死循环。 为防止这种情况,且保证格式化顺利进行,class-formatter 限制了执行嵌套深度,默认 50。超过深度的数据会停止转换。 可以通过 options.deep 自行调整深度。

死循环例:

@TransModel
class Person {
    @toString()
    name!: string;

    @toArray(Person)
    childs: Person = [{}];
}

executeTransArray(Person, [{}], {
    deep: 50
});

关于混入

为实现更加灵活的模板组合方案,class-formatter 提供 mixins 方法实现多模板组合。

例如:

@TransModel
class A {
    @toString()
    a!: string;
}

@TransModel
class B {
    @toString()
    b!: string;
}

@TransModel
class C implements A, B {
    a!: string;
    b!: string;

    @toString()
    c!: string;
}

mixins(C, [A, B]);

如此 C 便即继承了 AB 的全部指令。

同时提供了 Mixins 类装饰器来简化混入。

@TransModel
class A {
    @toString('A')
    name!: string;
}

@TransModel
class B {
    @toString('B')
    name!: string;
}

@Mixins(A, B)
@TransModel
class C implements A, B {
    @toString()
    c!: string;

    name!: string;
}

const res = executeTransform(C, {});
// res => { name: 'A', c: '' }

Mixins 传参拥有优先级,当多个模板中存在相同的属性时,后面参数的指令将会 完全覆盖 前一个参数的指令。 上述示例中,模板 B 的指令拥有最高优先级,A 模板中 name 属性的指令将会被 B 模板的 name 完全覆盖。

注意事项

关于执行顺序
为便于使用,装饰器的执行顺序为从上到下顺序执行。

@TransModel
class Test {
    @toString('张三')
    @Format(v => `我是${v}`)
    name!: string;
}

const res = executeTransform(Test, {});
// res => { name: '我是张三' }

关于使用

  • 所有转化规则均依赖指令,所有拥有指令的属性、方法、访问器均会被转换,其余属性、方法、访问器会被忽略。
  • 指令可以通过 Extend 在多个模板间继承。
  • 多个模板可通过 Mixins 组合成一个大模板,同时共享全部指令。
  • 子模板中声名的同名属性若拥有指令,则子模板的指令将会 完全覆盖 继承的指令。(即重写指令)
  • 子模板的 模板默认值 会覆盖父模板的 模板默认值