Home > Net >  How do I iterate through child objects and convert all non-object values to string
How do I iterate through child objects and convert all non-object values to string

Time:02-15

I currently have an existing method where I convert all key value pairs to string

export function convertObjValueToString<T>(data: Record<string, T>): Record<string, string> {
  return Object.keys(data).reduce((acc, key) => {
    return {
      ...acc,
      [key]: String(data[key]),
    };
  }, {});
}

but it is currently limited to the first level of an object. I want it to test deeper if for instance the value is another child object

{ 
    id: 2, 
    metadata: { 
        booleanValue: false, 
        someOtherKey: { 
            booleanValue: false
        }
    }
}

expected result would be:

{ 
    id: '2', 
    metadata: { 
        booleanValue: 'false', 
        someOtherKey: { 
            booleanValue: 'false'
        }
    }
}

CodePudding user response:

You can use Object.entries() to convert the object to an array of [key, value], map the array of pairs and transform according to type, and then convert back to an object using Object.fromEntries():

const convertObjValueToString = data => {
  if (Array.isArray(data)) return data.map(convertObjValueToString)

  if (typeof data === 'object') return Object.fromEntries(
    Object.entries(data)
    .map(([k, v]) => [k, convertObjValueToString(v)])
  )

  return String(data)
}


const obj = {
  id: 2,
  metadata: {
    booleanValue: false,
    someOtherKey: {
      booleanValue: false
    }
  }
}

const result = convertObjValueToString(obj)

console.log(result)

Types

You'll need to use a recursive type both to describe the original object, and then resulting object (TS Playground):

type NestedValues<T> =
  | T
  | { [property: string]: NestedValues<T> }
  | NestedValues<T>[];

const convertObjValueToString = (data: NestedValues<any>): NestedValues<string> => {
  if (Array.isArray(data)) return data.map(convertObjValueToString)

  if (typeof data === 'object') return Object.fromEntries(
    Object.entries(data)
    .map(([k, v]) => [k, convertObjValueToString(v)])
  )

  return String(data)
}

CodePudding user response:

export function convertObjValueToString<T extends object>(data: Record<string, T>): Record<string, string> {
  return Object.keys(data).reduce((acc, key) => {
    const currentPropValue = data[key];
    if(currentPropValue === Object(currentPropValue)) {
      return {
        ...acc,
        [key]: convertObjValueToString(currentPropValue)
      };
    }

    return {
      ...acc,
      [key]: String(data[key]),
    };
  }, {});
}

I got the solution right but I having problem fixing the types though as I am getting a argument of type 'T' is not assignable to parameter of type 'Record<string, object>' under my if condition

CodePudding user response:

Consider this example:

type Values = number | boolean | string
type Dictionary = { [prop: string]: Dictionary | Values }

const isObject = (data: unknown): data is Dictionary => 
  typeof data === 'object' && data !== null

type ObjToString<Obj extends Dictionary> = {
  [Prop in keyof Obj]:
  (Obj[Prop] extends Dictionary
    ? ObjToString<Obj[Prop]>
    : (Obj[Prop] extends Values
      ? `${Obj[Prop]}`
      : never)
  )
}

const record = <
  Key extends PropertyKey,
  Value
>(key: Key, value: Value) =>
  ({ [key]: value })

const merge = <
  Obj extends Dictionary,
  Part extends Dictionary
>(obj: Obj, part: Part) =>
  ({ ...obj, ...part })

const convert = <
  Data extends Dictionary
>(data: Data): ObjToString<Data> =>
  Object.keys(data).reduce((acc, elem) => {
    const value = data[elem];

    const maker = isObject(value) ? convert : String

    return merge(acc, record(elem, maker(value)))
  }, {} as ObjToString<Data>)


const result = convert({
  id: 2,
  metadata: {
    booleanValue1: false,
    someOtherKey: {
      booleanValue2: false
    }
  }
})

const id = result.id // `${number}`
const booleanValue2 = result.metadata.someOtherKey.booleanValue2 // "false"

Playground

Values - represents allowed primitive values of the object

Dictionary - represents allowed shape/interface of argument

isObject - custom typeguard. Check whether passed value is an object or not

ObjToString - type representation of business logic. Converts passed object to object where all primitive values are stringified

record - small helper (can be inlined)

merge - small helper (can be inlined)

convert - main function. Recursively goes through each key and converts value to string or makes recursive call

As you might have noticed result is infered and TS is aware which properties are allowed an which are not. Please keep in mind that I have passed literal value of object. If you pass a reference TS still infer allowed properties but values wll be more wider. For instance instead of "false" you will get "true"|"false"

If your argument should match some interface it is better because in that case TS will infer required properties more precisely

  • Related