Skip to content

Instantly share code, notes, and snippets.

@Nipsuli
Created October 29, 2025 16:09
Show Gist options
  • Select an option

  • Save Nipsuli/b18f462f91db828a95abeba7738865db to your computer and use it in GitHub Desktop.

Select an option

Save Nipsuli/b18f462f91db828a95abeba7738865db to your computer and use it in GitHub Desktop.
Flatten deep object with strongly typed keys
// Helpers to be used instead of Object | object | Any
export type Primitive = string | number | boolean | symbol | bigint;
export type KeyedObj = { [key: string]: Obj };
export type Obj = Primitive | Date | null | undefined | Obj[] | KeyedObj;
export type FlattenObjectArrayKeys<
T extends Record<string, unknown> | Primitive[] | Record<string, unknown>[],
Key = T extends Record<string, unknown> ? keyof T : `[${number}]`,
> = Key extends string
? T extends Record<string, unknown>
? T[Key] extends
| Record<string, unknown>
| Primitive[]
| Record<string, unknown>[]
? `${Key}.${FlattenObjectArrayKeys<T[Key]>}`
: `${Key}`
: T extends Primitive[]
? `${Key}`
: T extends Record<string, unknown>[]
? `${Key}.${FlattenObjectArrayKeys<T[number]>}`
: never
: never;
export const isPrimitive = (v: unknown): v is Primitive =>
typeof v === "string" ||
typeof v === "number" ||
typeof v === "boolean" ||
typeof v === "bigint" ||
typeof v === "symbol";
export const isDate = (value: unknown): value is Date => {
return value instanceof Date && !isNaN(value.getTime());
};
export const flatten = <
T extends
| Record<string | number | symbol, Obj>
| Record<string | number | symbol, Obj>[],
>(
obj: T,
) => {
const res = {} as Record<string, Primitive | Date | null | undefined>;
const r = (acc: string, o: Obj): void => {
if (isPrimitive(o) || isDate(o)) {
res[acc] = o;
} else if (Array.isArray(o)) {
for (const [i, e] of o.entries()) {
r(`${acc}${acc ? "." : ""}[${i.toString()}]`, e);
}
} else if (o !== null && o !== undefined) {
for (const [k, v] of Object.entries(o)) {
r(`${acc}${acc ? "." : ""}${k}`, v);
}
} else {
res[acc] = o;
}
};
r("", obj);
// Need to figure out if one could fix the internal types to
// match things from the getgo
return res as unknown as Record<
FlattenObjectArrayKeys<T>,
Primitive | Date | null | undefined
>;
};
/*
Example
*/
const tstObj = {
a: 1,
b: [2, 3],
c: {
d: 4,
e: [{
f: 5
}]
}
}
const flattenedTstObj = flatten(tstObj)
// Inferred type:
// Record<"a" | `b.[${number}]` | "c.d" | `c.e.[${number}].f`, Primitive | Date | null | undefined>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment