0

I want to know if it is possible to type this function. I am recursively checking for a type of value inside an object (shouldReplace is a type guard), and passing that value to the function fn

const replaceInObject = (shouldReplace, fn) => {
  const recurse = object => {
    for (const [key, val] of Object.entries(object)) {
      if (shouldReplace(val)) object[key] = fn(val)
      else if (Array.isArray(object)) object[key] = val.map(recurse)
      else if (typeof object === 'object') object[key] = recurse(val)
    }
  }
  return recurse
}

For clarity, here is an example usage:

const isEs6Map = (val: any): val is Map<any, any> => val instanceof Map
const es6MapToObject = <K, V>(map: Map<K, V>): { [key: string]: V } => {
  const obj: { [key: string]: V } = {}
  for (const [key, val] of map) obj[key.toString()] = val
  return obj
}
const marshal = replaceInObject(isEs6Map, es6MapToObject)

const sampleInput = {
  m: new Map([['a', 1], ['b', 2]]),
  meta: 'info',
  children: [
    {
      m: new Map([['c', 3], ['d', 4]]),
      meta: 'child'
    }
  ]
}

const serializedData = marshal(sampleInput)

The use case is essentially creating a generic marshalling function. I have objects stored in memory using Maps, and I want to send these across the network so that they can be viewed in a web ui. I need to convert all the classes into pure data objects so that they can be properly serialized by JSON.stringify.

If it is not possible to type this function, I can of course, accomplish the same task with several serializer function & types specific to each input object. I am hoping however that it is possible to avoid writing types whos output can be described with this simple function.

This stackoverflow question is related, but does not deal with recursion, or choosing which values to replace. Typescript: Generic object mapping function

andykais
  • 996
  • 2
  • 10
  • 27

1 Answers1

0

You have to apply recursive map on values of sampleInput.

So Input type looks like this: type Input = { meta: string, m: Map<string, number>, children?: Array<Input> };

Let's make generic type with take input and return parsed one:

type Parse<I extends Input> = {
    [K in keyof I]: I[K] extends Map<any, infer Value>
     ? { [key: string]: Value } : I[K] extends Array<any>
  ? Array<Parse<I[K][number]>> : I[K]
}

We need 3 cases: for string, Map and Array. By A extends B ? : syntax we can apply different logic based on type, so:

string => string
Map<K, V> => { [K]: V }
Array<Item> => Array<Parse<Item>

Playground

Kalle Richter
  • 8,008
  • 26
  • 77
  • 177
Przemyslaw Jan Beigert
  • 2,351
  • 3
  • 14
  • 19
  • thanks for the answer! This type would be generic to the output of the `marshal` function, but do you know a way to type the `replaceInObject` function? I am struggling with a way to pass a replaceable type whose values can be inferred (a.k.a. Map values) [gist to playground link](https://gist.github.com/andykais/37638d1a979c1b6dfb016651df5528c5) (playground link itself was too long) – andykais Feb 11 '19 at 21:11
  • I'm not sure that I understand you correctly, you want to replace Maps into objects and replace example values from this Map? – Przemyslaw Jan Beigert Feb 12 '19 at 07:47
  • Sorry if this isnt clear, yes that is my primary use case, but my original question is if I can type the `replaceInObject` function. The `replaceInObject` function does not need to replace Maps specifically. The `shouldReplace` function could be a type guard for anything, and the `fn` could replace that value with anything. I want to know if I could recreate your `Parse` type using a more generic `ReplaceInObject` type. – andykais Feb 12 '19 at 15:15
  • replaceInObject will looks like this: `let replaceIn = (input: I): Parse => {...}` and more generic will look like this `type ParseGeneric = { [K in keyof I]: I[K] extends Find ? { ReplaceWith } : I[K] extends Array ? Array> : I[K] }` Please tell me if that resolving your problem, I'll add it to my answer. – Przemyslaw Jan Beigert Feb 13 '19 at 21:18
  • Sort of, I was hoping for a generic `replaceInObject` function that could expect _any_ object, not just something derived from `Input`. I think that I am approaching a limitation of typescript though, after attempting to make your solution totally generic in my own playground. I will mark your answer as accepted though because it has given me a working solution. – andykais Feb 14 '19 at 02:04
  • To avoid `Input` type you can do this: `type ParseVeryGeneric = { [K in keyof T]: T[K] extends Find ? ReplaceWith : T[K] extends Array ? Array> : T[K] }` this will replace deeply array attributes – Przemyslaw Jan Beigert Feb 14 '19 at 10:18
  • yes, but if you open the playground link in my first comment, you will see the limitations of this type. You cannot infer members on `Find` in `ReplaceWith`. Meaning, the type created from `ParseVeryGeneric, Map` is _widened_ compared to `Parse`, where the values inside a map can be inferred. – andykais Feb 14 '19 at 15:57