我已经有了一个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
类型断言。
假设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
是键的元组类型,我们希望T
是具有K
属性的对象类型,这些属性的值可分配给{}
,即所谓的空对象类型,它(尽管名称)接受所有原语(如string
或number
),只拒绝null
和undefined
。我这样做是因为你在这些属性上调用了.toString()
,我们不希望可能-undefined
或null
属性在那里。
如果你想知道readonly XXX[]
,这是因为readonly
数组和readonly
元组比"normal"限制少。数组。(这些名字具有欺骗性;这里的readonly
表示"肯定可以读,但也可以不可以写",而常规数组是"肯定可以读,也肯定可以写"。
最后,keys
是[...K]
而不仅仅是K
,这是使用可变元组类型给编译器一个提示,我们希望它跟踪该数组的确切长度,因为它有很大的不同。
好的,现在我们来定义GroupByMulti
:
type GroupByMulti<T, K extends readonly any[]> =
K extends readonly [any, ...infer KR] ?
Record<string, GroupByMulti<T, KR>> : readonly T[];
这是一个遍历K
元组的递归条件类型。如果K
为空,我们只需要readonly T[]
,也就是说,不按键分组只给您一个数组。否则,我们想要Record<string, GroupByMulti<T, KR>>
,其中KR
是一个比K
短1的数组。(这是K
数组的R
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]
是数组)。
Playground链接到代码