如何正确定义嵌套对象的通用类型

时间:2019-04-11 17:26:18

标签: typescript

我正在尝试创建一个简单的递归函数,该函数将处理具有动态结构的对象,并且打字时遇到问题。

interface Nested {
    id: number;
    children?: Nested[];
}

interface Props<T> {
    elements: T[];
    childProp: string;
    idProp: string;
}

function recursive<T>(element: T, childProp: string, idProp: string) {
    console.log(element[idProp], childProp, element[childProp]);
    if (element[childProp]) {
        element[childProp].forEach((el: T) => {
            recursive<T>(el, childProp, idProp);
        });
    }
}

function test<T>(props: Props<T>) {
    props.elements.forEach((element) => {
        recursive<T>(element, props.childProp, props.idProp);
    });
}

const nested: Nested[] = [
    {
        id: 1,
        children: [
            {
                id: 2,
                children: [
                    {
                        id: 3
                    }
                ]
            },
            {
                id: 4,
                children: [

                ]
            },
        ]
    },
    {
        id: 5
    }
]

test<Nested>({
    elements: nested,
    childProp: 'children',
    idProp: 'id'
});

从技术上讲,代码可以工作,但是在recursive函数中,我得到了一个隐式的任何错误。嵌套对象将具有一些字段来指示其ID(并非始终是ID,可以是categoryId或其他任何名称),并且是一个可选字段,其中包含具有相同结构的对象数组(并非始终是子对象)。

问题出在

function recursive<T>(element: T, childProp: string, idProp: string) {
    console.log(element[idProp], childProp, element[childProp]);
    if (element[childProp]) {
        element[childProp].forEach((el: T) => {
            recursive<T>(el, childProp, idProp);
        });
    }
}

带有element[idProp]element[childProp]

1 个答案:

答案 0 :(得分:0)

recursive的原始定义中,泛型类型参数T完全不受约束,可以是任何东西。最重要的是,在类型级别上,当我们希望它们的值具有重要性时,childPropidProp并不能真正对具有此类通用类型(string)的类型做出贡献。也就是说,我们想要更多的文字类型。

尝试以下方法,尝试对我们正在寻找的对象的形状给出更通用的定义:

type MyElement<CKey extends string, IKey extends string>
  = { [K in CKey]?: MyElement<CKey, IKey>[] } & { [K in IKey]: number } & Record<string, any>;

{ [K in CKey]?: MyElement<CKey, IKey>[] }:将具有CKey命名的属性的对象描述为共享相同的CKeyIKey的子级的可选数组。

{ [K in IKey]: number }:将具有IKey命名的对象描述为number

Record<string, unknown>:描述具有未知类型的其他属性的对象。我们使用unknown使得使用它们会比any带来更好的错误,后者会悄无声息地使您脱离类型系统。这是说对象的其他属性很好。

然后,我们将两者与&放在一起,说该对象必须满足所有约束。看一个例子:

const t: MyElement<'children', 'testId'> = { testId: 30, children: [{ testId: 40 }] };

现在,我们可以更新recursive的签名以利用新的约束:

function recursive<CKey extends string, IKey extends string>(element: MyElement<CKey, IKey>, childProp: CKey, idProp: IKey) {
  console.log(element[idProp], childProp, element[childProp]);
  if (element[childProp]) {
    element[childProp].forEach(el => {
      recursive(el, childProp, idProp);
    });
  }
}

当然还有一些测试,以确保一切都按预期进行类型检查:

recursive({ testId: 10 }, 'children', 'testId');
recursive({ testId: 10, children: [], anyprop: 'something', date: new Date() }, 'children', 'testId');

// Expected error, children elements must have a `testId`
recursive({ testId: 10, children: [{}] }, 'children', 'testId');
recursive({ testId: 10, children: [{ testId: 13 }] }, 'children', 'testId');

recursive({ testId: 10, children: [{ testId: 13, children: [{ testId: 15 }] }] }, 'children', 'testId');
// Expected error, the deepest `children` must be an array our these id'd elements
recursive({ testId: 10, children: [{ testId: 13, children: {} }] }, 'children', 'testId');

the playground中试用!