简体   繁体   English

是否可以在 Typescript 中为每个映射类型设置一个泛型

[英]Is it possible to have a Generic for each Mapped Type in Typescript

I have a function that takes multiple reducers and applies them all to a data structure.我有一个函数,它需要多个减速器并将它们全部应用于数据结构。 For example, given two people person1 and person2 it can use this function to normalise their data:例如,给定两个人person1person2 ,它可以使用这个函数来规范化他们的数据:

normalizeData([person1, person2], {
    byId: {
      initialValue: {} as { [id: string]: Person },
      reduce: (acc, model) => {
        acc[model["id"]] = model
        return acc
      },
    },
    list: {
      initialValue: [] as Person[],
      reduce: (acc, model) => {
        acc.push(model)
        return acc
      },
    },
  })

Which will return something like:这将返回如下内容:

{
  byId: {
    1: {
      id: 1,
      name: "John",
    },
    2: {
      id: 2,
      name: "Jane",
    },
  },
  list: [
    {
      id: 1,
      name: "John",
    },
    {
      id: 2,
      name: "Jane",
    },
  ],
}

I am having trouble implementing these types in Typescript because I want each property that I pass in to use the type of initialValue as the type for the accumulator acc in the reduce callback.我在 Typescript 中实现这些类型时遇到问题,因为我希望传入的每个属性都使用initialValue的类型作为 reduce 回调中累加器acc的类型。

Is it possible to have this sort of inferred generic type on a mapped type?是否可以在映射类型上使用这种推断的泛型类型?

Here is a link to the full code in a runnable example 这是可运行示例中完整代码的链接

Reproducible example可重现的例子

// Same type as Array.reduce callback
type ReduceCallback<Value, Output> = (
  previousValue: Output,
  currentValue: Value,
  currentIndex: number,
  array: Value[],
) => Output

// Type for sample data
type Person = {
  id: string
  name: string
  parentId?: string
  age: number
}

// Function to run multiple reducers over an array of data
// This is the function I want to type properly
export function normalizeData<Model, ReducerKeys extends string, InitialValue>(
  data: Model[],
  reducers: {
    [key in ReducerKeys]: {
      reduce?: ReduceCallback<Model, InitialValue>
      initialValue: InitialValue
    }
  },
) {
  // Get keys of reducers to split them into two data structures, 
  // one for initial values and the other for reduce callbacks
  const reducerKeys = Object.keys(reducers) as Array<keyof typeof reducers>

  // Get an object of { id: <initialValue> }
  // In this case `{ byId: {}, list, [] }`
  const initialValues = reducerKeys.reduce(
    (obj, key) => ({
      ...obj,
      [key]: reducers[key].initialValue,
    }),
    {} as { [key in ReducerKeys]: InitialValue },
  )

  // Get an array of reduce callbacks
  const reduceCallbacks = reducerKeys.map((key) => ({ key, callback: reducers[key].reduce }))

  // Reduce over the data, applying each reduceCallback to each datum
  const normalizedData = data.reduce((acc, datum, index, array) => {
    return reduceCallbacks.reduce((acc, { key, callback }) => {
      const callbackWithDefault = callback || ((id) => id)
      return {
        ...acc,
        [key]: callbackWithDefault(acc[key], datum, index, array),
      }
    }, acc)
  }, initialValues)

  return normalizedData
}

// Sample data
const parent: Person = {
  id: "001",
  name: "Dad",
  parentId: undefined,
  age: 53,
}
const son: Person = {
  id: "002",
  name: "Son",
  parentId: "001",
  age: 12,
}

// This is the test implementation.
// The types do not accept differing generic types of initialValue for each mapped type
// Whatever is listed first sets the InitialValue generic
// I want to be able to have the intialValue type for each mapped type 
// apply that same type to the `acc` value of the reduce callback.
normalizeData([parent, son], {
  byId: {
    initialValue: {} as {[key: string]: Person},
    reduce: (acc, person) => {
      acc[person.id] = person
      return acc
    },
  },
  list: {
    initialValue: [] as Person[],
    reduce: (acc, person) => {
      acc.push(person)
      return acc
    },
  },
})

Let's start with the function declaration.让我们从函数声明开始。 I think you can remove the ReducerKeys generic type.我认为您可以删除ReducerKeys泛型类型。 We can instead rename InitialValue to InitialValues to indicate that we will store an object type inside there with each key being a specific InitialValue .我们可以改为将InitialValue重命名为InitialValues ,以表明我们将在其中存储一个对象类型,每个键都是一个特定的InitialValue

export function normalizeData<Model, InitialValues>(
  data: Model[],
  reducers: {
    [K in keyof InitialValues]: {
      reduce?: ReduceCallback<Model, InitialValues[K]>
      initialValue: InitialValues[K]
    }
  },
) { /* ... */ }

Instead of mapping over ReducerKeys , we map over InitialValues and store the type of initialValue inside InitialValues[K] .我们不是在ReducerKeys上映射,而是在InitialValues上映射并将initialValue的类型存储在InitialValues[K]中。

This already fixed the typing when calling the function.这已经修复了调用函数时的输入。

We now get an error inside the function implementation because RecucerKeys does not exist anymore.我们现在在函数实现中得到一个错误,因为RecucerKeys不再存在。 Let's just replace it with InitialValues too.让我们也用InitialValues替换它。

const initialValues = reducerKeys.reduce(
  (obj, key) => ({
    ...obj,
    [key]: reducers[key].initialValue,
  }),
  {} as { [K in keyof InitialValues]: InitialValues[K] },
)

Playground 操场

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM