likes
comments
collection
share

前端静态枚举字典的优雅实践(TS版本)绝对优雅!

作者站长头像
站长
· 阅读数 58

前言

在上一篇中,用js实现枚举数组的扩展类,在这一篇中,将使用ts实现枚举数组扩展类,并使用ts类型编程实现更加优雅的类型提示!

js版本: juejin.cn/post/721242…

定义枚举

在定义枚举的数组方式和js一样。

const sexEnum = [
  {
    label: '男',
    value: 0
  },
  {
    label: '女',
    value: 1
  }
]

在vsCode中,将鼠标移动到变量sexEnum上,会出现ts的类型提示。

前端静态枚举字典的优雅实践(TS版本)绝对优雅!

实际上,枚举对象的每个value和label值都可以是单独的字面量类型,在ts中可以使用 as const 将此枚举断言为一个只读常量,将整个对象设为字面量类型。

前端静态枚举字典的优雅实践(TS版本)绝对优雅!

枚举数组中所有value转为联合类型

将枚举类型缩减为字面量类型,可以做到很多的事情,比如在后端返回的sex字段中,在ts的项目里一般是将sex字段定义为 number0 | 1这样的类型,虽然这样也行,但不够优雅,实际上sex的类型应该由刚刚创建sexEnum枚举的value值来生成实时的联合类型。这样再新加一个value:“未知”,label:2的性别时,就不用在0 | 1这样的类型中补上2这个值了。

接下来就开始实现这个将枚举数组中value值转为联合类型的工具类型。因技术原因,ts类型写得不是特别好,如果有ts大佬希望能给个更优雅的工具类型。

然后就一步一步将这工具类型完成,不在意过程的可以在最后直接用现成的,如果有帮助希望最后给个免费的赞。

首先,从枚举数组里的对象入手,将对象中value提取出来。先写一个能提取出对象的任意索引值的工具类型。

/**
 * 提取对象属性类型
 */
type GetObjType<T extends Record<string, any>, K extends keyof T> = T extends {
  [key in K]: infer R
}
  ? R
  : never

前端静态枚举字典的优雅实践(TS版本)绝对优雅!

在拿到了对象的value的类型后,在实现从数组提取出第一个对象,并返回该对象的value的工具类型。

export type GetArrOneProp<Arr extends readonly Record<string, any>[]> = Arr extends readonly [
  infer First,
  ...infer
]
  ? First
  : never

前端静态枚举字典的优雅实践(TS版本)绝对优雅!

拿到了数组的第一个对象后,在使用上面实现的GetObjType工具类型来提取当前对象的value类型。

export type GetArrOneProp<Arr extends readonly Record<string, any>[]> = Arr extends readonly [
  infer First,
  ...infer
]
  ? GetObjType<First & Record<string, any>, 'value'>
  : never

前端静态枚举字典的优雅实践(TS版本)绝对优雅! 现在已经可以拿到枚举数组中第一个对象的value的类型,接下只要在将剩下的枚举对象的value类型取出就可以实现该功能了。

在ts里可以使用递归的方式来循环数组,将枚举数组中所有对象的value组合成联合类型。

/** 将数组中所有对象的 vlaue 转换为联合类型 */
export type GetObjValuesType<Arr extends readonly Record<string, any>[]> = Arr extends readonly [
  infer First,
  ...infer Rest
]
  ?
      | GetObjType<First & Record<string, any>, 'value'>
      | GetObjValuesType<Rest & Record<string, any>[]>
  : never

前端静态枚举字典的优雅实践(TS版本)绝对优雅!

定义枚举数组的扩展类

根据上面对定义枚举的方式,写一个枚举的类型接口。

interface EnumArrayObj {
  value: number | string
  label: string
}

如果项目中其他参数也可以在接口中添加对应的类型信息。

接下来就开始实现枚举的扩展类!

export class EnumArray<T extends readonly EnumArrayObj[]> extends Array<EnumArrayObj> {
  constructor(list: T) {
    super(...list)
    Object.setPrototypeOf(this, EnumArray.prototype)
  }

  /** 根据值获取标签 */
  getLabelByValue(value: GetObjValuesType<T>) {
    return this.find((item) => item.value === value)?.label || '-'
  }
  /** 根据标签获取值 */
  getValueByLabel(label: GetObjLabelsType<T>) {
    return this.find((item) => item.label === label)?.value || '-'
  }
  /** 根据值获取对应的枚举对象 */
  getCorrespondEnumObjByValue(value: GetObjValuesType<T>) {
    return this.find((item) => item.value === value)
  }
}

其中GetObjLabelsType这个是和GetObjValuesType差不多的工具类型,将枚举数组中所有label,转为联合类型。相信各位都知道怎么写吧。

使用枚举数组类

使用new的方式创建一个枚举数组,将鼠标移动到变量上能看到枚举详细信息。

前端静态枚举字典的优雅实践(TS版本)绝对优雅!

在调用sexEnumgetLabelByValue时,可以看到当前的value的类型提示。

前端静态枚举字典的优雅实践(TS版本)绝对优雅!

枚举工厂函数

封装一个工厂函数创建枚举数组

export function createEnum<T extends readonly EnumArrayObj[]>(enums: T) {
  return Object.freeze(new EnumArray(enums))
}

使用

const sexEnum = createEnum([
  {
    label: '男',
    value: 1
  },
  {
    label: '女',
    value: 2
  }
] as const)

类型信息依旧保留。

前端静态枚举字典的优雅实践(TS版本)绝对优雅!

使用枚举createEnum创建的枚举数组在使用GetObjValuesType来将所有的value类型转为联合类型时会失效,返回的是never类型。

这是因为使用createEnum创建的枚举数组返回值类型还嵌着ReadonlyEnumArray两个类型,所以只要把嵌在这个两个类型里的数组类型提取出来就行了。

export type GetEnumArrayType<T> = T extends Readonly<EnumArray<infer P>>
  ? GetObjValuesType<P>
  : never

前端静态枚举字典的优雅实践(TS版本)绝对优雅!

完整代码

// 枚举类型接口
interface EnumArrayObj {
  value: number | string
  label: string
}

// 提取对象属性类型
type GetObjType<T extends Record<string, any>, K extends keyof T> = T extends {
  [key in K]: infer R
}
  ? R
  : never

/** 将数组中所有对象的 vlaue 转换为联合类型 */
export type GetObjValuesType<Arr extends readonly Record<string, any>[]> = Arr extends readonly [
  infer First,
  ...infer Rest
]
  ?
      | GetObjType<First & Record<string, any>, 'value'>
      | GetObjValuesType<Rest & Record<string, any>[]>
  : never

/** 将数组中所有对象的 label 转换为联合类型 */
export type GetObjLabelsType<Arr extends readonly Record<string, any>[]> = Arr extends readonly [
  infer First,
  ...infer Rest
]
  ?
      | GetObjType<First & Record<string, any>, 'label'>
      | GetObjLabelsType<Rest & Record<string, any>[]>
  : never

// 枚举数组扩展类
class EnumArray<T extends readonly EnumArrayObj[]> extends Array<EnumArrayObj> {
  constructor(list: T) {
    super(...list)
    Object.setPrototypeOf(this, EnumArray.prototype)
  }

  /** 根据值获取标签 */
  getLabelByValue(value: GetObjValuesType<T>) {
    return this.find((item) => item.value === value)?.label || '-'
  }

  /** 根据标签获取值 */
  getValueByLabel(label: GetObjLabelsType<T>) {
    return this.find((item) => item.label === label)?.value || '-'
  }

  /** 根据值获取对应的枚举对象 */
  getCorrespondEnumObjByValue(value: GetObjValuesType<T>) {
    return this.find((item) => item.value === value)
  }
}

// 创建枚举的扩展类
function createEnum<T extends readonly EnumArrayObj[]>(enums: T) {
  return Object.freeze(new EnumArray(enums))
}

// 获取枚举数组value的联合类型
type GetEnumArrayType<T> = T extends Readonly<EnumArray<infer P>> ? GetObjValuesType<P> : never

// 创建性别枚举
export const sexEnum = createEnum([
  {
    label: '男',
    value: 1
  },
  {
    label: '女',
    value: 2
  }
] as const)

转载自:https://juejin.cn/post/7233228344417402938
评论
请登录