【问题标题】:Type 'number' is not assignable to type 'T[keyof T]', but 'T[keyof T]' is number类型“数字”不可分配给类型“T[keyof T]”,但“T[keyof T]”是数字
【发布时间】:2020-06-05 09:39:38
【问题描述】:

解决这个问题的正确方法是什么:

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]

但我觉得这里有更深层次的东西在起作用。

【问题讨论】:

    标签: typescript


    【解决方案1】:

    一个问题是T extends Partial&lt;Record&lt;string, number&gt;&gt;接受属性比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 的映射。因此,如果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&lt;Record&lt;K, number&gt;&gt;(或等效的{[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

    【讨论】:

      猜你喜欢
      • 2019-11-17
      • 2021-11-10
      • 2021-07-19
      • 2019-01-08
      • 2020-12-16
      • 2021-08-26
      • 2020-10-28
      • 1970-01-01
      • 2016-07-31
      相关资源
      最近更新 更多