带有嵌套对象的Object.keys():获取正确的类型

时间:2019-05-31 15:13:57

标签: typescript

我有3个对象abc,每个对象都有一个prop字段,其值可以是任何值。

const a = { prop: { foo: 'bar', bar: true } }
const b = { prop: { baz: 1234 } }
const c = { prop: true }

这些对象分为几部分:

const sectionA = { a, b }
const sectionB = { c }
const allSections = {sectionA, sectionB}

我想动态构造以下对象:

const myObject = {
  sectionA: {
    a: {foo: 'bar', bar: true},
    b: {baz: 1234}
  },
  sectionB: {
    c: true
  }
}

即正确地组织成嵌套对象,但没有prop字段。

这就是我所做的:

type ObjectWithProp = { prop: any }

type ObjectTypes<Section extends {[index:string]: ObjectWithProp}> = {
    [K in keyof Section]: Section[K]['prop']
}

type AllSectionTypes<AllSection extends { [index: string]: { [index: string]: ObjectWithProp } }> = {
    [K in keyof AllSection]: ObjectTypes<AllSection[K]>
}

const result = Object.keys(allSections).reduce((outerAcc, _key) => {
    const key = _key as keyof typeof allSections;
    const section = allSections[key];

    outerAcc[key] = Object.keys(section).reduce((innerAcc, _objectName) => {
        const objectName = _objectName as keyof typeof section;
        const obj = section[_objectName];

        innerAcc[objectName] = obj.prop;

        return innerAcc;
    }, {} as ObjectTypes<typeof section>);

    return outerAcc;
}, {} as AllSectionTypes<typeof allSections>)

[Link to TS Playground]

在行const objectName = _objectName as keyof typeof section;上,objectName不幸地是never(逻辑上是因为对象之间没有公共字段)。但是后来我做不到innerAcc[objectName]

我该如何解决?

1 个答案:

答案 0 :(得分:0)

注意:以下内容在TS3.4 +中有效,因为它依赖于higher order type inference from generic functions

这就是我要做的。这是一个相当大的重构,主要是因为所有嵌套类型都伤害了我的大脑。您可能可以将类型从其中复制到原始代码中,但是我不会尝试这样做。但是,实际行为几乎是相同的(例如,使用Object.keys().reduce()而不是for循环):

// a technically unsafe version of Object.keys(o) that assumes that 
// o only has known properties of T
function keys<T extends object>(o: T) {
  return Object.keys(o) as Array<keyof T>;
}

// Turn {k1: {prop: v2}, k3: {prop: v4} into {k1: v2, k3: v4}
function pullOutProp<TK extends Record<keyof TK, { prop: any }>>(o: TK) {
  return keys(o).reduce(
    <P extends keyof TK>(
      acc: { [P in keyof TK]: TK[P]['prop'] },
      k: P
    ) => (acc[k] = o[k].prop, acc),
    {} as { [P in keyof TK]: TK[P]['prop'] });
}

// Turn {k1: {k2: {prop: v3}, k4: {prop: v5}}, k6: {k7: {prop: v8}}} into
// {k1: {k2: v3, k4: v5}, k6: {k7: v8}}
function nestedPullOutProp<T extends { 
  [K in keyof T]: Record<keyof T[K], { prop: any }> }
>(o: T) {
  return keys(o).reduce(
    <K extends keyof T>(
      acc: { [K in keyof T]: { [P in keyof T[K]]: T[K][P]['prop'] } },
      k: K
    ) => (acc[k] = pullOutProp(o[k]), acc),
    {} as { [K in keyof T]: { [P in keyof T[K]]: T[K][P]['prop'] } }
  )
}

const result = nestedPullOutProp(allSections); // your desired result

您可以验证result是您期望的类型。这里的技巧基本上是使pullOutProp()nestedPullOutProp()尽可能通用,对具有最低功能要求的类型进行操作(例如,T extends { [K in keyof T]: Record<keyof T[K], { prop: any }> }>意味着将存在t.x.y.prop只要t.x.y存在),然后使对reduce()的回调通用。在每一步骤中,泛型类型都足够直接让编译器遵循逻辑,而不必抱怨分配问题……尤其是因为对TS3.4中引入的泛型函数中的高阶类型推断进行了改进。

仅在最后,当您调用nestedPullOutProp(allSections)时,编译器实际上会继续尝试评估泛型,此时泛型将成为预期的类型。

好的,希望能有所帮助;祝你好运!

Link to code