UNPKG

@agilebot/decamelize-keys

Version:
186 lines (162 loc) 4.22 kB
import type {DelimiterCase} from 'type-fest'; // eslint-disable-next-line @typescript-eslint/ban-types type EmptyTuple = []; /** Return a default type if input type is nil. @template T - Input type. @template U - Default type. */ type WithDefault<T, U extends T> = T extends undefined | void | null ? U : T; // eslint-disable-line @typescript-eslint/ban-types // TODO: Replace this with https://github.com/sindresorhus/type-fest/blob/main/source/includes.d.ts /** Check if an element is included in a tuple. */ type IsInclude<List extends readonly unknown[], Target> = List extends undefined ? false : List extends Readonly<EmptyTuple> ? false : List extends readonly [infer First, ...infer Rest] ? First extends Target ? true : IsInclude<Rest, Target> : boolean; /** Convert the keys of an object from camel case. */ export type DecamelizeKeys< T extends Record<string, any> | readonly any[], Separator extends string = '_', Exclude extends readonly unknown[] = EmptyTuple, Deep extends boolean = false, StopPaths extends readonly string[] = EmptyTuple, > = T extends readonly any[] // Handle arrays or tuples. ? { [P in keyof T]: T[P] extends Record<string, any> | readonly any[] // eslint-disable-next-line @typescript-eslint/ban-types ? {} extends DecamelizeKeys<T[P], Separator> ? T[P] : DecamelizeKeys< T[P], Separator, Exclude, Deep, StopPaths > : T[P]; } : T extends Record<string, any> // Handle objects. ? { [ P in keyof T as [IsInclude<Exclude, P>] extends [true] ? P : DelimiterCase<P, Separator> ]: Record<string, unknown> extends DecamelizeKeys<T[P]> ? T[P] : [Deep] extends [true] ? DecamelizeKeys< T[P], Separator, Exclude, Deep, StopPaths > : T[P]; } // Return anything else as-is. : T; type Options<Separator> = { /** The character or string used to separate words. Important: You must use `as const` on the value. @default '_' @example ``` import decamelizeKeys from 'decamelize-keys'; decamelizeKeys({fooBar: true}); //=> {foo_bar: true} decamelizeKeys({fooBar: true}, {separator: '-' as const}); //=> {'foo-bar': true} ``` */ readonly separator?: Separator; /** Exclude keys from being camel-cased. If this option can be statically determined, it's recommended to add `as const` to it. @default [] */ readonly exclude?: ReadonlyArray<string | RegExp>; /** Recurse nested objects and objects in arrays. @default false @example ``` import decamelizeKeys from 'decamelize-keys'; decamelizeKeys({fooBar: true, nested: {unicornRainbow: true}}, {deep: true}); //=> {foo_bar: true, nested: {unicorn_rainbow: true}} ``` */ readonly deep?: boolean; /** Exclude children at the given object paths in dot-notation from being decamel-cased. For example, with an object like {a: {b: '🦄'}}, the object path to reach the unicorn is 'a.b'. If this option can be statically determined, it's recommended to add `as const` to it. @default [] @example ``` import decamelizeKeys from 'decamelize-keys'; decamelizeKeys({ aB: 1, aC: { cD: 1, cE: { eF: 1 } } }, { deep: true, stopPaths: [ 'aC.cE' ] }), // { // a_b: 1, // a_c: { // c_d: 1, // c_e: { // eF: 1 // } // } // } ``` */ readonly stopPaths?: readonly string[]; }; /** Convert object keys from camel case using [`decamelize`](https://github.com/sindresorhus/decamelize). @param input - Object or array of objects to decamelize. @example ``` import decamelizeKeys from 'decamelize-keys'; // Convert an object decamelizeKeys({fooBar: true}); //=> {foo_bar: true} // Convert an array of objects decamelizeKeys([{fooBar: true}, {barFoo: false}]); //=> [{foo_bar: true}, {bar_foo: false}] ``` */ export default function decamelizeKeys< T extends Record<string, any> | readonly any[], Separator extends string = '_', OptionsType extends Options<Separator> = Options<Separator>, >( input: T, options?: Options<Separator> ): DecamelizeKeys< T, Separator, WithDefault<OptionsType['exclude'], EmptyTuple>, WithDefault<OptionsType['deep'], false>, WithDefault<OptionsType['stopPaths'], EmptyTuple> >;