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"
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