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

Flatten object with custom keys in Typescript

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

例如:

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

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

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

<Component name="red" /> //is correct, but
<Component name=`something different than "red" or "green"` /> //is incorrect.

展平函数:

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
}

首先,如果你想让编译器有机会意识到 Type"red" | "green" 而不是 string,我们必须确保 myObjct的类型包括这些 literal types. The easy way to do that is with a const assertion:

const myObject = {
  names: {
    title: 'red',
    subtitle: 'green'
  },
} as const;

接下来,您的 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 */ }

它使用 recursive conditional types, template literal types, and key remapping in mapped types 递归遍历所有属性并将键与中间的 . 连接在一起。我不知道这个特定类型的函数是否适用于 flatten() 的所有用例,我不敢假装它没有潜伏在其中的恶魔。


如果我使用该签名,并使用上面的 const 断言 myObject 调用 flatten(myObject),您会得到:

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

万岁!这足以让 Type 成为 "red" | "green":

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

也万岁。


我想如果你真的不关心跟踪键名,你可以通过返回带有 string index signature:

的东西来大大简化 flatten() 类型签名
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 */ }

产生

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

最终

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

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

Playground link to code