我想找到一种方法,在 TypeScript 中使用一种类型,该类型将接受某些对象类型并获取所有嵌套的关键路径,包括可能是数组的任何属性,并对这些属性进行索引,并在这些对象内的任何对象上获取这些属性数组。然而,我不希望出现任何这些嵌套数组的内置属性(如“push”、“pop”等)。我已经看到了许多执行此操作的解决方案,但这并不完全是我的意思寻找。如果我有这个物体:
const person = {
name: 'Jim',
address: {
street: '1234 Some Street',
city: 'Some City',
state: 'Some State'
},
hobbies: [
{
name: 'Sports'
}
]
}
type RESULT = DeepKeys<typeof person>
,我希望这些关键路径是
name | address | address.street | address.city | address.state | hobbies | hobbies.${number} | hobbies[${number}] | hobbies.${number}.name | hobbies[${number}].name
是否可以在不获取联合体中所有无效数组方法和任何额外键的情况下执行此操作?
我尝试了网上找到的多种类型,但没有一个能完全满足这一点。
好吧,所以我之前肯定写过这类事情,请参阅Typescript:嵌套对象的深层keyof和其他类似的问题,但是像这样的深度递归类型always有奇怪的边缘情况,并且不同的人似乎对于在这些情况下正确的行为应该是什么有截然不同的看法。因此,接下来我将针对问题中的特定示例。对于其他情况,它可能不会达到读者的期望。修复这种深度递归类型有时可能涉及相当彻底的重构。当心。
基本方法如下所示:
type DeepKeys<T> = T extends object ? (
{ [K in (string | number) & keyof T]:
`${(
`.${K}` | (`${K}` extends `${number}` ? `[${K}]` : never)
)}${"" | DeepKeys<T[K]>}` }[
(string | number) & keyof T]
) : never
这是一个分布式对象类型(在microsoft/TypeScript#47109中创造),其形式为
{[K in KK]: F<K>}[KK]
,是一个映射类型,我立即索引以获得F<K>
的union对于工会
K
中的所有 KK
。在这种情况下,KK
是(string | number) & keyof T
,表示symbol
的所有可序列化(非T
)键。 F<K>
是一个很大的 模板文字 形式的东西
`${(`.${K}` | (`${K}` extends `${number}` ? `[${K}]` : never))}${"" | DeepKeys<T[K]>}`
它有两个部分连接在一起。左边的部分是
(`.${K}` | (`${K}` extends `${number}` ? `[${K}]` : never))
。它始终包含以点 .
为前缀的键。如果 K
是类似数字的,那么它还包括方括号 [
⋯]
中的键。右边的部分是"" | DeepKeys<T[K]>}
。它包含空字符串(因此包含点/括号内的键,没有后缀)和 DeepKeys<T[K]>
(因此它递归地将当前属性的键添加为后缀)。
这个递归的东西让我们大部分完成了任务。让我们在您的示例中尝试一下:
type Result = DeepKeys<typeof person>
/* type Result = ".name" | ".address" | ".address.street" | ".address.city" |
".address.state" | ".hobbies" | `.hobbies.${number}` | `.hobbies[${number}]` |
`.hobbies.${number}.name` | `.hobbies[${number}].name` | ".hobbies.length" |
".hobbies.toString" | ".hobbies.toLocaleString" | ".hobbies.pop" | ".hobbies.push" |
".hobbies.concat" | ".hobbies.join" | ".hobbies.reverse" | ".hobbies.shift" |
".hobbies.slice" | ".hobbies.sort" | ".hobbies.splice" | ".hobbies.unshift" |
".hobbies.indexOf" | ".hobbies.lastIndexOf" | ".hobbies.every" | ".hobbies.some" |
".hobbies.forEach" | ".hobbies.map" | ".hobbies.filter" | ".hobbies.reduce" |
".hobbies.reduceRight" | ".hobbies.find" | ".hobbies.findIndex" | ".hobbies.fill" |
".hobbies.copyWithin" | ".hobbies.entries" | ".hobbies.keys" |
".hobbies.values" | ".hobbies.includes" */
所以这里的两个问题是:
让我们解决这些问题。首先,我们可以将
T
包装在 FixArr<T>
中,检查 T
是否为数组,如果是,则忽略所有数组中存在的每个属性,除了 number
之外(因为我们希望输出中包含 number
) )。像这样:
type FixArr<T> = T extends readonly any[] ? Omit<T, Exclude<keyof any[], number>> : T;
接下来,我们可以编写一个类型来删除字符串中的任何初始点,并将其作为输出。
type DropInitDot<T> = T extends `.${infer U}` ? U : T;
因此,让我们将旧的
DeepKeys
重命名为 _DeepKeys
,一种辅助类型,并使用 FixArr<T[K]>
代替 T[K]
:
type _DeepKeys<T> = T extends object ? (
{ [K in (string | number) & keyof T]:
`${(
`.${K}` | (`${K}` extends `${number}` ? `[${K}]` : never)
)}${"" | _DeepKeys<FixArr<T[K]>>}` }[
(string | number) & keyof T]
) : never
然后
DeepKeys
是
type DeepKeys<T> = DropInitDot<_DeepKeys<FixArr<T>>>
因此,我们确保修复
T
(如果它是数组),对结果执行 _DeepKeys
,然后 DropInitDot
它。这给了我们
type Result = DeepKeys<typeof person>
/* type Result = "name" | "address" | "hobbies" |
"address.street" | "address.city" | "address.state" |
`hobbies.${ number } ` | `hobbies[${ number }]` |
`hobbies.${ number }.name` | `hobbies[${ number }].name` */
随心所欲。