Question
Typings to flatten an object based on its id and label properties
I want to flatten an object with the following shape
{
article: 'prova',
id: 63,
topology: { id: 'topId', label: 'topLabel' },
something: { id: 'someId', label: 'someLabel' }
}
into something with the following one
{
article: "prova",
id: 63,
topId: "topLabel",
someId: "someLabel"
}
Both the Input
and the Output
types must satisfy strict typings. Basically the following must hold true:
interface Input {
article: string
id: number
abc: { id: string; label: string }
def: { id: string; label: string }
}
interface Output {
article: string
id: number
topId: string
someId: string
}
const input1: Input = {
article: 'prova',
id: 63,
abc: { id: 'topId', label: 'topLabel' },
def: { id: 'someId', label: 'someLabel' }
}
const input2 = {
article: 'prova',
id: 63,
abc: { id: 'topId', label: 'topLabel' },
def: { id: 'someId', label: 'someLabel' }
}
// Type 'Flattened<string | number | null | undefined, { id: string; label: string; }, Record<string, string | number | { id: string; label: string; } | null | undefined>>' is missing the following properties from type 'Output': article, id, topId, someId
// Argument of type 'Input' is not assignable to parameter of type 'Record<string, string | number | { id: string; label: string; } | null | undefined>'.
// Index signature for type 'string' is missing in type 'Input'.
const output1: Output = flattenObject(input1)
// Type 'Flattened<string | number | null | undefined, { id: string; label: string; }, { article: string; id: number; abc: { id: string; label: string; }; def: { id: string; label: string; }; }>' is missing the following properties from type 'Output': topId, someId
const output2: Output = flattenObject(input2)
I've tried this tentative implementation but TypeScript isn't happy:
function hasId<
K extends string | number | undefined | null,
T extends { id: string; label: string }
>(el: [string, K | T]): el is [string, T] {
return (el[1] as T).id != null
}
type Flattened<
K extends string | number | undefined | null,
T extends { id: string; label: string },
S extends Record<string, K | T>
> = {
[key in keyof S as S[key] extends T ? S[key]['id'] : key]: S[key] extends T
? S[key]['label']
: S[key]
}
const flattenObject = <
K extends string | number | undefined | null,
T extends { id: string; label: string },
S extends Record<string, K | T>
>(
obj: S
): Flattened<K, T, S> =>
Object.fromEntries(
Object.entries(obj).map<[string, K | string]>((el) =>
hasId(el) ? [el[1].id, el[1].label] : (el as [string, K])
)
) as Flattened<K, T, S>