我已经有一个 groupBy 函数,它接收一个对象数组和一个键。它能够按单个属性进行分组。
const groupBy = <T extends Record<string, unknown>, U extends keyof T>(
objArr: T[],
key: U,
): { [key: string]: T[] }
由于我得到了一个 depth=1
对象,因此我对返回类型非常满意 { [key: string]: T[] }
但现在我需要扩展此函数,以便能够按多个属性进行分组并使用 depth=keys.length
创建嵌套对象。
groupBy
函数:
const groupBy = <T extends Record<string, unknown>, U extends keyof T>(
objArr: T[],
key: U,
): { [key: string]: T[] } => objArr
.reduce((memo, x) => {
if (x[key]) {
const value = (x[key] as any).toString();
if (!memo[value]) {
memo[value] = [];
}
memo[value].push(x);
}
return memo;
}, {} as { [key: string]: Array<T> });
groupByMulti
函数,仅递归调用 groupBy
函数,直到到达最后一个分组键。
const groupByMulti = <T extends Record<string, unknown>, U extends keyof T>(
arr: T[],
keys: U[],
propIndex = 0,
) => {
const grouppedObj = groupBy(arr, keys[propIndex]);
Object.keys(grouppedObj).forEach((key) => {
if (propIndex < keys.length - 1) {
grouppedObj[key] = groupByMulti(grouppedObj[key], keys, propIndex + 1);
}
});
return grouppedObj;
}
我想知道是否可以构建具有固定深度的对象类型,例如:
groupByMulti(someArray, ['key1', 'key2']): {
[key: string]: {
[key: string]: T[]
}
}
// Depending on the keys length, for example
groupByMulti(someArray, ['key1', 'key2', 'key3']): {
[key: string]: {
[key: string]: {
[key: string]: T[]
}
}
}
我知道只有当键是 ReadonlyArray 时这才有可能,但我可以接受。 如果不可能,我怎样才能在这里实现类型安全?
转换为JS
的示例,举例说明:
const cars = [{
car: 'Audi',
model: 'A6',
style: 'Sedan',
year: '2005',
},
{
car: 'Audi',
model: 'A4',
style: 'Sedan',
year: '2018',
},
{
car: 'Toyota',
model: 'Corola',
style: 'Sedan',
year: '2006',
},
{
car: 'Toyota',
model: 'Camry',
style: 'Sedan',
year: '2006',
},
]
const groupBy = (
objArr,
property,
) => objArr
.reduce((memo, x) => {
if (x[property]) {
const value = (x[property]).toString();
if (!memo[value]) {
memo[value] = [];
}
memo[value].push(x);
}
return memo;
}, {});
const groupByMulti = (arr, keys, propIndex = 0) => {
var grouppedObj = groupBy(arr, keys[propIndex]);
Object.keys(grouppedObj).forEach((key) => {
if (propIndex < keys.length - 1) {
grouppedObj[key] = groupByMulti(grouppedObj[key], keys, propIndex + 1);
}
});
return grouppedObj;
}
console.log(JSON.stringify(groupByMulti(cars, ['car', 'year', 'model']), null, 2));
最佳答案
我假设您更关心调用者方面的类型安全,特别是因为您现有的 groupBy()
函数至少有一个as any
type assertion在其实现中。
假设 groupByMulti()
的调用签名应该看起来像:
<T extends Record<K[number], {}>, K extends readonly (keyof T)[]>(
arr: readonly T[],
keys: readonly [...K],
propIndex?: number
) => GroupByMulti<T, K>
了解 GroupByMulti
的一些适当定义.
在我们开始GroupByMulti
之前,让我解释一下该调用签名的其余部分:
我们想要K
成为tuple type key ,我们想要 T
是属性为 K
的对象类型这些属性的值可分配给 {}
,即所谓empty object type它(尽管有名称)接受所有原语(例如 string
或 number
)并且仅拒绝 null
和undefined
。我这样做是因为你打电话 .toString()
在这些属性上,我们可能不希望- undefined
或null
属性在那里。
如果您想知道 readonly XXX[]
,这是因为 readonly
arrays和 readonly
tuples比“普通”数组的限制更少。 (名字是骗人的;readonly
这里的意思是“绝对可以读,但可能会也可能不会写”,而常规数组是“一定可以读,并且一定可以写”)。
最后一个事实是keys
是 [...K]
而不仅仅是K
正在使用 variadic tuple types给编译器一个提示,我们希望它跟踪该数组的确切长度,因为它有很大的不同。
好的,现在让我们定义 GroupByMulti
:
type GroupByMulti<T, K extends readonly any[]> =
K extends readonly [any, ...infer KR] ?
Record<string, GroupByMulti<T, KR>> : readonly T[];
这是一个 recursive conditional type走过K
元组。如果K
是空的,我们只想要readonly T[]
,也就是说,不按键分组只会给你一个数组。否则,我们想要 Record<string, GroupByMulti<T, KR>>
哪里KR
是一个比 K
短 1 的数组。 (这是 R
数组的 K
est,所以我称之为 KR
。🤷♂️)。
该实现充满了类型断言,我不会花时间解释:
const groupByMulti = <
T extends Record<K[number], {}>,
K extends readonly (keyof T)[]
>(arr: readonly T[], keys: readonly [...K], propIndex = 0) => {
var grouppedObj: any = groupBy(arr, keys[propIndex]);
Object.keys(grouppedObj).forEach((key) => {
if (propIndex < keys.length - 1) {
grouppedObj[key] = groupByMulti<any, any[]>(
grouppedObj[key], keys, propIndex + 1);
} else {
grouppedObj[key] = grouppedObj[key]
}
});
return grouppedObj as GroupByMulti<T, K>;
}
唯一重要的一点是我们返回 GroupByMulti<T, K>
类型的值。让我们测试一下:
const result = groupByMulti(cars, ['car', 'year', 'model']);
/* const result: Record<string, Record<string, Record<string, readonly {
car: string;
model: string;
style: string;
year: string;
}[]>>> */
Object.keys(result).forEach(k =>
Object.keys(result[k]).forEach(l =>
Object.keys(result[k][l]).forEach(m => {
result[k][l][m].forEach(c => console.log(c.car));
})
)
)
看起来不错。 result
的类型是一个三重嵌套对象 string
键,其深层对象类型是 T
的 an对象。这使我们能够以某种类型安全的方式处理结果(编译器知道 result[k][l][m]
是一个数组)。
关于typescript - 如何在 typescript 中构建类型安全的多属性groupBy,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/72968324/