// Flat snake_case <-> camelCase key mappers. Top-level keys only: values are copied by reference // and their types pass through untouched (T[K]). Deep case-convert libraries recurse into value // types, which mangles branded primitives (a branded string is `string & {...}`, so it reads as an // object and gets walked); staying flat sidesteps that and keeps the keys' value types exact. type SnakeToCamel = S extends `${infer Head}_${infer Tail}` ? `${Head}${Capitalize>}` : S; type CamelToSnake = S extends `${infer Head}${infer Tail}` ? Head extends Uppercase ? Head extends Lowercase ? `${Head}${CamelToSnake}` // non-letter (digit, etc.): leave as-is : `_${Lowercase}${CamelToSnake}` // uppercase letter: split with an underscore : `${Head}${CamelToSnake}` // lowercase letter: keep : S; export type CamelKeys = {[K in keyof T as SnakeToCamel]: T[K]}; export type SnakeKeys = {[K in keyof T as CamelToSnake]: T[K]}; const toCamel = (key: string): string => key.replace(/_([a-z])/g, (_full, letter: string) => letter.toUpperCase()); const toSnake = (key: string): string => key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`); export function camelKeys(row: T): CamelKeys { return Object.fromEntries(Object.entries(row).map(([key, value]) => [toCamel(key), value])) as CamelKeys; } export function snakeKeys(model: T): SnakeKeys { return Object.fromEntries(Object.entries(model).map(([key, value]) => [toSnake(key), value])) as SnakeKeys; }