在 Typescript 中使用自定义键展平对象

Bar*_*ski 5 flatten typescript

我想扁平化对象并将返回值转换为类型。

例如:

const myObject = {
  names: {
    title: 'red',
    subtitle: 'green'
  },
}
const returned = [...Object.values(flatten(myObject))] as const
// returns ['red', 'green']
type Type = typeof returned[number]
Run Code Online (Sandbox Code Playgroud)

现在返回的变量是 ['red', 'green']

类型应为“红色|” 'green',但现在是一个字符串数组,因为返回的 typeof 是 string[]。我想使用这种类型为我的组件输入 prop:

<Component name="red" /> //is correct, but
<Component name=`something different than "red" or "green"` /> //is incorrect.
Run Code Online (Sandbox Code Playgroud)

压平函数:

type FlattenedObject = { [x: string]: string }

export const flattenDaisyChain = (obj: any): FlattenedObject => {
  const result: FlattenedObject = {}

  const transform = (wrapper: FlattenedObject | string, p?: string) => {
    switch (typeof wrapper) {
      case 'object':
        p = p ? p + '.' : ''
        for (const item in wrapper) {
          transform(wrapper[item], p + item)
        }
        break
      default:
        if (p) {
          result[p] = wrapper
        }
        break
    }
  }

  transform(obj)

  return result
}
Run Code Online (Sandbox Code Playgroud)

jca*_*alz 14

首先,如果您希望编译器有机会认识到 thatType"red" | "green"与 相对的string,我们必须确保 thatmyObjct的类型包含这些文字类型。最简单的方法是使用const断言

const myObject = {
  names: {
    title: 'red',
    subtitle: 'green'
  },
} as const;
Run Code Online (Sandbox Code Playgroud)

接下来,您的flatten()函数的类型只能在 TypeScript 4.1+ 中勉强表示。我可能会这样写,使用类似问题的答案:

type Flatten<T extends object> = object extends T ? object : {
    [K in keyof T]-?: (x: NonNullable<T[K]> extends infer V ? V extends object ?
        V extends readonly any[] ? Pick<T, K> : Flatten<V> extends infer FV ? ({
            [P in keyof FV as `${Extract<K, string | number>}.${Extract<P, string | number>}`]:
            FV[P] }) : never : Pick<T, K> : never
    ) => void } extends Record<keyof T, (y: infer O) => void> ?
    O extends infer U ? { [K in keyof O]: O[K] } : never : never

const flatten = <T extends object>(obj: T): Flatten<T> => { /* impl */ }
Run Code Online (Sandbox Code Playgroud)

它使用递归条件类型模板文字类型映射类型中的键重新映射来递归地遍历所有属性并将键连接.在一起。我不知道这个特定类型的函数是否适用于 的所有用例flatten(),而且我不敢假装它没有潜伏着恶魔。


如果我使用该签名,并flatten(myObject)使用上面的const-asserted进行调用myObject,您会得到以下结果:

const flattenedMyObject = flatten(myObject);
/* const flattenedMyObject: {
    readonly "names.title": "red";
    readonly "names.subtitle": "green";
} */
Run Code Online (Sandbox Code Playgroud)

万岁!这些信息Type足以"red" | "green"

const returned = [...Object.values(flatten(myObject))] as const
type Type = typeof returned[number] // "red" | "green"
Run Code Online (Sandbox Code Playgroud)

也万岁。


我想如果您并不真正关心跟踪键名称,您可以flatten()通过返回带有string索引签名的内容来大大简化类型签名:

type DeepValueOf<T> = object extends T ? object :
    T extends object ? DeepValueOf<
        T[Extract<keyof T, T extends readonly any[] ? number : unknown>]
    > : T

const flatten = <T extends object>(obj: T): { [k: string]: DeepValueOf<T> } => {
    /* impl */ }
Run Code Online (Sandbox Code Playgroud)

产生

const flattenedMyObject = flatten(myObject);
/* const flattenedMyObject: {
   [k: string]: "green" | "red";
 } */
Run Code Online (Sandbox Code Playgroud)

最终

const returned = [...Object.values(flatten(myObject))] as const
type Type = typeof returned[number] // "red" | "green"
Run Code Online (Sandbox Code Playgroud)

我并不担心DeepValueOfFlatten但如果没有一些边缘情况,我会感到惊讶。因此,在任何类型的生产代码中使用它之前,您需要进行真正的测试。

Playground 代码链接