/** * Convert a snake_case string to camelCase. * @example "account_id" -> "accountId", "created_at" -> "createdAt" */ export function snakeToCamelString(s: string): string { return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase()); } /** * Recursively convert an object's keys from snake_case to camelCase. * Arrays and nested objects are traversed; primitives, Date, and RegExp are returned as-is. */ export function snakeToCamelObjectDeep(obj: T): SnakeToCamel { if (obj === undefined || obj === null) { return obj as SnakeToCamel; } if (typeof obj !== "object") { return obj as SnakeToCamel; } if (obj instanceof RegExp || obj instanceof Date) { return obj as SnakeToCamel; } if (Array.isArray(obj)) { return obj.map(snakeToCamelObjectDeep) as SnakeToCamel; } return Object.fromEntries( Object.entries(obj).map(([key, value]) => [ snakeToCamelString(key), typeof value === "object" && value !== null && !Array.isArray(value) ? snakeToCamelObjectDeep(value) : Array.isArray(value) ? value.map(snakeToCamelObjectDeep) : value, ]), ) as SnakeToCamel; } /** * Convert a snake_case string to camelCase at the type level. */ export type SnakeToCamelString = S extends `${infer A}_${infer B}` ? `${A}${Capitalize>}` : S; /** * Recursively convert an object type's keys from snake_case to camelCase. */ export type SnakeToCamel = T extends object ? T extends Array ? Array> : T extends Date | RegExp | Function ? T : { [K in keyof T as K extends string ? SnakeToCamelString : K]: SnakeToCamel; } : T;