一个问题是T extends Partial<Record<string, number>>接受属性比number更窄的类型,例如numeric literal types:
type Month = { days: 28 | 29 | 30 | 31; }
const months: Month[] = [{ days: 31 }, { days: 28 }, { days: 31 }, { days: 30 }];
const days = getSumMapAndCount({ data: months, keys: ["days"] })[0].days;
// const days: 28 | 29 | 30 | 31
console.log(days); // 120 ?
这里,Month 具有 days 属性,该属性必须是 28 到 31 之间的整数。如果我们将Month 对象数组传递给getSumMapAndCount() 并要求它对days 求和,我们会得到一个元组,其第一个元素声称是Month。但它不是一个:它有 120 天。哎呀。
我认为情况会变得更糟。如果您传入的数据数组具有未包含在keys 数组中的键,则getSumMapAndCount() 返回一个元组,其第一个元素声称与数据的类型相同。但它会缺少键:
type Point2D = { x: number, y: number };
const points: Point2D[] = [{ x: 1, y: 1 }, { x: 2, y: 2 }, { x: 3, y: 3 }];
const point2D = getSumMapAndCount({ data: points, keys: ["x"] })[0];
// const point2D: Point2D;
point2D.y.toFixed(); // no compiler error, runtime ?
也糟糕。
我认为我们需要退后一步,仔细考虑输入和输出的类型,并正确编写函数的类型,然后努力让实现接受它。这是我认为你的打字:
function getSumMapAndCount<K extends PropertyKey,
T extends { [P in K]?: number }>({ data, keys }: {
data: readonly T[],
keys: readonly K[]
}): [{ [P in K]?: number }, number] {
对于联合 K 中与 keys 数组的元素相对应的每个键,我们仅将 T 限制为不具有该键或在该键处具有 number 属性。我们不关心任何其他键。重要的是,输出类型只是声称是从K 中的键(某些子集)到number 的映射。因此,如果T 是Month,而K 是"days",则输出的第一个元素将是{days?: number} 而不是Month。那挺好的。让我们看看在实现中会发生什么:
let dataCount = 0
const dataSumMap = data.reduce((dataMap, datum) => {
const keysToAdd = keys.filter(k => k in datum); // changed this
// Only count if at least one key exists
if (keysToAdd.length === 0) {
return dataMap
}
dataCount++
keysToAdd.forEach(key => {
const sum = dataMap[key] as number || 0
const value = datum[key] as number || 0
dataMap[key] = sum + value;
})
return dataMap
}, {} as { [P in K]?: number })
return [dataSumMap, dataCount]
}
唯一真正的区别是我断言初始映射是Partial<Record<K, number>>(或等效的{[P in K]?: number})而不是T。然后一切正常。 (请注意,我没有 lodash,所以我将您对 intersection 的使用更改为数组过滤器;在那里做您想做的事情)。让我们看看它是否有效:
const days = getSumMapAndCount({ data: months, keys: ["days"] })[0].days;
// const days: number | undefined
console.log(days); // 120 ?
const justX = getSumMapAndCount({ data: points, keys: ["x"] })[0];
// const justX: {x?: number | undefined} ?
justX.y.toFixed(); // compiler error!
// ~ <-- there's no y on {x?: number | undefined}
现在看起来好多了。
好的,希望对您有所帮助;祝你好运!
Playground link to code