使用 TypeScript 实现类型安全的动态分组求和
在前端数据处理场景中,我们经常需要对数组数据按照指定字段进行分组,再对分组后的数值字段求和。如果使用原生 JavaScript 实现,很容易因为字段类型不匹配、字段不存在等问题出现运行时错误。TypeScript 的类型系统可以帮助我们在编译阶段就规避这些问题,实现类型安全的动态分组求和逻辑。
需求分析
我们希望实现的函数需要满足以下要求:
- 接收一个对象数组作为数据源,数组元素类型统一
- 支持动态指定分组依据的字段,该字段必须是对象中存在的属性
- 支持动态指定求和的字段,该字段必须是对象中存在的数值类型属性
- 返回分组后的结果,每个分组包含分组键和对应的求和值
类型定义与实现
首先我们需要定义通用的类型,约束函数的入参和返回值,确保类型安全:
// 定义基础对象类型,键为字符串,值为任意类型
type BaseObj = Record<string, any>;
// 提取对象中值为数值类型的键
type NumericKeys<T extends BaseObj> = {
[K in keyof T]: T[K] extends number ? K : never;
}[keyof T];
// 提取对象中所有键
type AllKeys<T extends BaseObj> = keyof T;
// 分组求和结果类型
interface GroupSumResult<T extends BaseObj, GroupKey extends AllKeys<T>, SumKey extends NumericKeys<T>> {
groupKey: T[GroupKey];
sum: number;
}
// 分组求和函数类型定义
function dynamicGroupSum<
T extends BaseObj,
GroupKey extends AllKeys<T>,
SumKey extends NumericKeys<T>
>(
data: T[],
groupField: GroupKey,
sumField: SumKey
): GroupSumResult<T, GroupKey, SumKey>[] {
// 使用 Map 存储分组中间结果,键为分组值,值为累加和
const groupMap = new Map<T[GroupKey], number>();
for (const item of data) {
const groupValue = item[groupField];
const sumValue = item[sumField];
// 这里 TypeScript 已经通过类型约束确保 sumValue 是 number 类型
const currentSum = groupMap.get(groupValue) || 0;
groupMap.set(groupValue, currentSum + sumValue);
}
// 将 Map 转换为结果数组
const result: GroupSumResult<T, GroupKey, SumKey>[] = [];
for (const [groupKey, sum] of groupMap.entries()) {
result.push({
groupKey,
sum
});
}
return result;
}上面的代码中,我们通过泛型约束实现了类型安全:NumericKeys 工具类型会自动筛选出对象中值为 number 类型的键,避免我们传入非数值字段作为求和字段;AllKeys 类型确保分组字段必须是对象中存在的属性,如果传入不存在的字段,TypeScript 会在编译阶段直接报错。
使用示例
我们定义一个销售数据的类型,然后调用上面的函数进行测试:
// 定义销售数据类型
interface SaleRecord {
id: number;
product: string;
category: string;
amount: number;
count: number;
}
// 模拟销售数据
const saleList: SaleRecord[] = [
{ id: 1, product: '手机', category: '电子产品', amount: 3000, count: 1 },
{ id: 2, product: '电脑', category: '电子产品', amount: 8000, count: 1 },
{ id: 3, product: '衬衫', category: '服装', amount: 200, count: 5 },
{ id: 4, product: '裤子', category: '服装', amount: 300, count: 3 },
{ id: 5, product: '平板', category: '电子产品', amount: 4000, count: 2 },
];
// 按 category 分组,对 amount 求和
const categoryAmountSum = dynamicGroupSum(saleList, 'category', 'amount');
console.log('按品类分组销售额求和:', categoryAmountSum);
// 输出:[
// { groupKey: '电子产品', sum: 15000 },
// { groupKey: '服装', sum: 500 }
// ]
// 按 category 分组,对 count 求和
const categoryCountSum = dynamicGroupSum(saleList, 'category', 'count');
console.log('按品类分组销量求和:', categoryCountSum);
// 输出:[
// { groupKey: '电子产品', sum: 4 },
// { groupKey: '服装', sum: 8 }
// ]如果尝试传入不存在的字段,比如把分组字段写成 'type',或者把求和字段写成 'product'(字符串类型),TypeScript 会直接提示类型错误,避免运行时出现问题:
// 以下代码会在编译阶段报错 // 错误:'type' 不在 SaleRecord 的键中 // dynamicGroupSum(saleList, 'type', 'amount'); // 错误:'product' 不是数值类型的键 // dynamicGroupSum(saleList, 'category', 'product');
扩展到多字段求和
如果需要同时对多个字段求和,我们可以调整函数支持传入求和字段数组,返回的结果中包含所有求和字段的结果:
// 多字段求和结果类型
interface MultiGroupSumResult<
T extends BaseObj,
GroupKey extends AllKeys<T>,
SumKeys extends NumericKeys<T>[]
> {
groupKey: T[GroupKey];
sums: {
[K in SumKeys[number]]: number;
};
}
// 多字段分组求和函数
function dynamicMultiGroupSum<
T extends BaseObj,
GroupKey extends AllKeys<T>,
SumKeys extends NumericKeys<T>[]
>(
data: T[],
groupField: GroupKey,
sumFields: [...SumKeys]
): MultiGroupSumResult<T, GroupKey, SumKeys>[] {
const groupMap = new Map<T[GroupKey], Record<string, number>>();
for (const item of data) {
const groupValue = item[groupField];
let currentSums = groupMap.get(groupValue);
if (!currentSums) {
// 初始化所有求和字段为 0
currentSums = {} as Record<string, number>;
for (const field of sumFields) {
currentSums[field as string] = 0;
}
groupMap.set(groupValue, currentSums);
}
// 累加每个求和字段的值
for (const field of sumFields) {
currentSums[field as string] += item[field];
}
}
const result: MultiGroupSumResult<T, GroupKey, SumKeys>[] = [];
for (const [groupKey, sums] of groupMap.entries()) {
result.push({
groupKey,
sums: sums as MultiGroupSumResult<T, GroupKey, SumKeys>['sums']
});
}
return result;
}
// 多字段求和使用示例
const multiSumResult = dynamicMultiGroupSum(saleList, 'category', ['amount', 'count']);
console.log('多字段分组求和结果:', multiSumResult);
// 输出:[
// { groupKey: '电子产品', sums: { amount: 15000, count: 4 } },
// { groupKey: '服装', sums: { amount: 500, count: 8 } }
// ]通过这种方式,我们可以在 TypeScript 项目中实现灵活、类型安全的动态分组求和逻辑,减少运行时错误,提升代码的健壮性。
TypeScript动态分组求和类型安全泛型约束前端数据处理 本作品最后修改时间:2026-05-22 16:21:57