解决这个问题的正确方法是:
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]
}问题是Tkeyof T可以是number | undefined,那么为什么它会出错呢?我知道你可以用
dataMap[key] = (sum + value) as T[keyof T]但我感觉这里有更深层次的东西在起作用。
发布于 2020-06-05 09:39:38
一个问题是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之间(包括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] {对于与keys数组元素对应的联合K中的每个键,我们只约束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}现在看起来好多了。
好的,希望这会有帮助;祝你好运!
https://stackoverflow.com/questions/62206138
复制相似问题