TypeScript使用枚举封装和装饰器优雅的定义字典

3,522 阅读5分钟

banner.jpg

一、封装前的枚举字典定义

在日常开发中,我们经常会定义字典来做些下拉选择框:

  • 定义枚举
  • 定义字典

代码如下:

enum ArithmeticEnum {
  AES = 1,
  RSA = 2,
  NO = 3,
}
const ArithmeticEnumMap = {
  [ArithmeticEnum.AES]: 'AES算法',
  [ArithmeticEnum.RSA]: 'RSA算法',
  [ArithmeticEnum.NO]: '明文',
}

console.log(ArithmeticEnumMap[ArithmeticEnum.AES])
for (const key in ArithmeticEnumMap) {
  console.log(key, ArithmeticEnumMap[key])
}

然后将 ArithmeticEnumMap 传入到下拉组件中去循环遍历出选项。

<select>
  <option v-for="(value, key) in ArithmeticEnumMap" :key="key" :value="key">
    {{ value }}
  </option>
</select>

上述的操作比较正常,但写起来不够优雅,我们接下来按枚举封装和装饰器的思路来完成这个事情。

二、定义枚举基类

首先,枚举一般都有固定格式的数据属性,例如 key(枚举值),label(枚举名称) 等。并且会内置很多的方法,比如 get(key: number)getLabel(key: number)toArray() 等。

那么我们首先来定义这个枚举基类:

/**
 * # 类包装
 * @author Hamm.cn
 */
type ClassConstructor<T = any> = {
  // eslint-disable-next-line no-unused-vars
  new(...args: any[]): T;
}

/**
 * # 枚举基类
 * @author Hamm.cn
 */
export class AirEnum {
  /**
   * ## 枚举的值
   */
  key!: number

  /**
   * ## 枚举的描述
   */
  label!: string

  /**
   * ## 实例化创建一个枚举项目
   * @param key 枚举值
   * @param label 枚举描述
   */
  constructor(key: number, label: string) {
    this.key = key
    this.label = label
  }

  /**
   * ## 获取枚举的 `Label`
   * @param key `Key`
   * @param defaultLabel `可选` 默认 `-`
   */
  static getLabel(key: number, defaultLabel = '-'): string {
    return this.get(key)?.label || defaultLabel
  }

  /**
   * ## 查找一个枚举选项
   * @param key `Key`
   */
  static get<E extends AirEnum>(this: ClassConstructor<E>, key: number): E | null {
    return (this as any).toArray()
      .find((item: E) => item.key === key) || null
  }

  /**
   * ## 将枚举转为数组
   * @returns 枚举数组
   */
  // eslint-disable-next-line no-unused-vars
  static toArray<E extends AirEnum>(this: ClassConstructor<E>): E[] {
    return Object.values(this)
      .filter((item) => item instanceof this)
  }

  /**
   * ## 判断 `Key` 是否相等
   * @param key `Key`
   */
  equalsKey(key: number): boolean {
    return this.key === key
  }

  /**
   * ## 判断 `Key` 是否不相等
   * @param key `Key`
   */
  notEqualsKey(key: number): boolean {
    return this.key !== key
  }
}

在上述基类中,我们定义了之前所说的一些好常用方法以及内置的属性,接下来我们只需要在枚举类中继承这个基类,然后添加对应的静态属性即可。

三、定义枚举字典

/**
 * # 应用加密算法
 * @author Hamm.cn
 */
export class ArithmeticEnum extends AirEnum {
  static readonly AES = new ArithmeticEnum(1, 'AES算法')

  static readonly RSA = new ArithmeticEnum(2, 'RSA算法')

  static readonly NO = new ArithmeticEnum(3, '明文')
}

// 如何使用
console.log(ArithmeticEnum.AES.key)
console.log(ArithmeticEnum.AES.label)
console.log(ArithmeticEnum.getLabel(2))
console.log(ArithmeticEnum.toArray())

四、枚举封装和装饰器的碰撞

我们可以配合装饰器,来完成一些枚举类到使用组件的绑定:

class User{
  nickname!: string

  /**
   * ## 用户密码加密方式
   */
  @Dict(ArithmeticEnum)
  @Form({
    dict: ArithmeticEnum
  })
  @Table()
  @Search()
  arithmetic!: ArithmeticEnum
}

接下来,只要使用到用户密码加密方式的地方,都可以直接从 User 类中获取到对应的枚举字典,而无需再手动维护。

上述装饰器和组件的封装本文就不过多演示了,可以参考我们的 AirPower4T 开源项目中关于这部分的实现:AirPower On Github

五、扩展枚举

5.1 扩展属性

实际使用过程中,我们可能经常会在原有 key,label 属性之外定义更多业务需要的字典数据,例如 color,我们可以直接在枚举中进行扩展:

/**
 * # 应用加密算法
 * @author Hamm.cn
 */
export class ArithmeticEnum extends AirEnum {
  /**
   * ## 加密算法的颜色
   * 这是个自定义的属性。
   */
  color: string
  
  static readonly AES = new ArithmeticEnum(1, 'AES算法','red')

  static readonly RSA = new ArithmeticEnum(2, 'RSA算法','black')

  static readonly NO = new ArithmeticEnum(3, '明文','green')

  constructor(key: number, label: string, color: string) {
    super(key, label)
    this.color = color
  }


  /**
   * ## 获取枚举的 `Color`
   * @param key `Key`
   * @param defaultLabel `可选` 默认 ``
   */
  static getColor(key: number, defaultColor = ''): string {
    return this.get(key)?.color || defaultColor
  }
}

// 如何使用
console.log(ArithmeticEnum.getColor(2))

5.2 扩展枚举值类型

我们无法保证所有枚举的 key 都是数字,于是我们可以定义一个类型并配合泛型来支持这种场景:

type AirEnumKey = string | number | boolean

export class AirEnum<T extends AirEnumKey = number> {
  key: T
  // 省略更多代码...
}

接下来,无论定义什么类型的枚举,我们的封装类都完成了优雅的支持。

六、完整代码示例


/**
 * # 枚举基类
 * @author Hamm.cn
 */
export class AirEnum<K extends AirEnumKey = number> implements IDictionary {
  /**
   * ## 枚举的值
   */
  key!: K

  /**
   * ## 枚举的描述
   */
  label!: string

  /**
   * ## 标准 `AirColor` 颜色或自定义颜色
   * 支持 `AirColor` `标准色` `十六进制` `HTML标准色`
   */
  color?: AirColorString

  /**
   * ## 是否被禁用
   *  如禁用, 下拉选项中将显示但无法选中
   */
  disabled?: boolean

  /**
   * ## 实例化创建一个枚举项目
   * @param key 枚举值
   * @param label 枚举描述
   * @param color `可选` 枚举扩展的颜色
   * @param disable `可选` 是否禁用
   */
  constructor(key: K, label: string, color?: AirColorString, disable?: boolean) {
    this.key = key
    this.label = label
    this.color = color
    this.disabled = disable
  }

  /**
   * ## 获取枚举的 `Label`
   * @param key `Key`
   * @param defaultLabel `可选` 默认的标签
   */
  static getLabel(key: AirEnumKey, defaultLabel = AirConstant.HYPHEN): string {
    return this.get(key)?.label || defaultLabel
  }

  /**
   * ## 获取枚举的颜色
   * @param key `Key`
   * @param defaultColor `可选` 默认颜色
   */
  static getColor(key: AirEnumKey, defaultColor: AirColorString = AirColor.NORMAL): AirColorString {
    return this.get(key)?.color || defaultColor
  }

  /**
   * ## 获取枚举是否禁用
   * @param key `Key`
   */
  static isDisabled(key: AirEnumKey): boolean | undefined {
    return this.get(key)?.disabled
  }

  /**
   * ## 查找一个枚举选项
   * @param key `Key`
   */
  static get<E extends AirEnum<AirEnumKey>>(this: ClassConstructor<E>, key: AirEnumKey): E | null {
    return (this as AirAny).toArray()
      .find((item: E) => item.key === key) || null
  }

  /**
   * ## 将枚举转为数组
   * @returns 枚举数组
   */
  // eslint-disable-next-line no-unused-vars
  static toArray<K extends AirEnumKey, E extends AirEnum<K>>(this: ClassConstructor<E>): E[] {
    return Object.values(this)
      .filter((item) => item instanceof this)
  }

  /**
   * ## 将枚举转为字典
   * @returns 枚举字典
   */
  // eslint-disable-next-line no-unused-vars
  static toDictionary<D extends IDictionary>(this: ClassConstructor<D>): AirDictionaryArray<D> {
    return AirDictionaryArray.createCustom<D>(Object.values(this)
      .filter((item) => item instanceof this))
  }

  /**
   * ## 判断 `Key` 是否相等
   * @param key `Key`
   */
  equalsKey(key: K): boolean {
    return this.key === key
  }

  /**
   * ## 判断 `Key` 是否不相等
   * @param key `Key`
   */
  notEqualsKey(key: K): boolean {
    return this.key !== key
  }
}


/**
 * # 开放应用加密方式
 * @author Hamm.cn
 */
export class OpenAppArithmeticEnum extends AirEnum {
  static readonly AES = new OpenAppArithmeticEnum(1, 'AES', AirColor.WARNING)

  static readonly RSA = new OpenAppArithmeticEnum(2, 'RSA', AirColor.SUCCESS)

  static readonly NO = new OpenAppArithmeticEnum(3, '明文', AirColor.NORMAL)
}

七、总结

因为群里有小伙伴讨论到了这个问题,于是趁着上午没什么事情就水了这篇文章,很多的实现在 AirPower4T 项目中都有示例代码,有兴趣的朋友可以参考我们的开源项目,也欢迎各位大大的 Star ✨✨✨

Github: github.com/HammCn/AirP…

也欢迎阅读我们的专栏:“用TypeScript写前端”

That's all,各位周一愉快~