类型“ number”不可分配给类型“ T [keyof T]”,但是“ T [keyof T]”是数字

时间:2020-06-05 00:15:55

标签: typescript

解决此问题的正确方法是什么

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]

但是我感觉这里还有更深的作用。

1 个答案:

答案 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属性在那个关键。我们不在乎其他任何键。重要的是,输出类型仅声称是从Knumber中的键(某些子集)的映射。因此,如果TMonth,而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