解决此问题的正确方法是什么
function getSumMapAndCount<T extends Partial<Record<string, number>>>({
data,
keys
}: {
data: readonly T[],
keys: readonly (keyof T)[]
}): [T, number] {
let dataCount = 0
const dataSumMap = data.reduce<T>((dataMap, datum) => {
const keysToAdd = _.intersection(Object.keys(datum), keys)
// 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 // Type 'number' is not assignable to type 'T[keyof T]'.ts(2322)
})
return dataMap
}, {} as T)
return [dataSumMap, dataCount]
}
问题在于T [keyof T]可以为number | undefined
,所以为什么会出错?我知道您可以使用
dataMap[key] = (sum + value) as T[keyof T]
但是我感觉这里还有更深的作用。
答案 0 :(得分:0)
一个问题是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}
现在看起来好多了。
好的,希望能有所帮助;祝你好运!