├── .npmrc ├── .gitattributes ├── .gitignore ├── .github ├── security.md └── workflows │ └── main.yml ├── .editorconfig ├── index.test-d.ts ├── license ├── package.json ├── test.js ├── readme.md ├── index.js └── index.d.ts /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.github/security.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 18 14 | - 16 15 | - 14 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - run: npm install 22 | - run: npm test 23 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import {expectType, expectAssignable, expectNotType} from 'tsd'; 2 | import decamelizeKeys, {type DecamelizeKeys} from './index.js'; 3 | 4 | expectType<{foo_bar: boolean}>(decamelizeKeys({fooBar: true})); 5 | 6 | // Array 7 | expectType>(decamelizeKeys([{fooBar: true}])); 8 | 9 | // Custom separator 10 | expectType<{'foo-bar': boolean}>(decamelizeKeys({fooBar: true}, {separator: '-' as const})); 11 | 12 | // TODO: Port more tests from https://github.com/sindresorhus/camelcase-keys/blob/main/index.test-d.ts 13 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "decamelize-keys", 3 | "version": "2.0.1", 4 | "description": "Convert object keys from camel case", 5 | "license": "MIT", 6 | "repository": "sindresorhus/decamelize-keys", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "type": "module", 14 | "exports": { 15 | "types": "./index.d.ts", 16 | "default": "./index.js" 17 | }, 18 | "sideEffects": false, 19 | "engines": { 20 | "node": ">=14.16" 21 | }, 22 | "scripts": { 23 | "test": "xo && ava && tsd" 24 | }, 25 | "files": [ 26 | "index.js", 27 | "index.d.ts" 28 | ], 29 | "keywords": [ 30 | "map", 31 | "object", 32 | "key", 33 | "keys", 34 | "decamelize", 35 | "decamelcase", 36 | "uncamelcase", 37 | "camelcase", 38 | "camel-case", 39 | "camel", 40 | "case", 41 | "separator", 42 | "string", 43 | "text", 44 | "convert", 45 | "deep", 46 | "recurse", 47 | "recursive" 48 | ], 49 | "dependencies": { 50 | "decamelize": "^6.0.0", 51 | "map-obj": "^4.3.0", 52 | "quick-lru": "^6.1.1", 53 | "type-fest": "^3.1.0" 54 | }, 55 | "devDependencies": { 56 | "ava": "^5.0.1", 57 | "tsd": "^0.24.1", 58 | "xo": "^0.52.4" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable quote-props */ 2 | import test from 'ava'; 3 | import decamelizeKeys from './index.js'; 4 | 5 | test('main', t => { 6 | t.deepEqual(Object.keys(decamelizeKeys({fooBar: true})), ['foo_bar']); 7 | }); 8 | 9 | test('separator option', t => { 10 | t.deepEqual(Object.keys(decamelizeKeys({fooBar: true}, {separator: '-'})), ['foo-bar']); 11 | }); 12 | 13 | test('exclude option', t => { 14 | t.deepEqual(Object.keys(decamelizeKeys({'--': true}, {exclude: ['--']})), ['--']); 15 | t.deepEqual(Object.keys(decamelizeKeys({fooBar: true}, {exclude: [/^f/]})), ['fooBar']); 16 | }); 17 | 18 | test('deep option', t => { 19 | t.deepEqual( 20 | decamelizeKeys({fooBar: true, obj: {oneTwo: false, arr: [{threeFour: true}]}}, {deep: true}), 21 | {'foo_bar': true, obj: {'one_two': false, arr: [{'three_four': true}]}}, 22 | ); 23 | }); 24 | 25 | test('handles nested arrays', t => { 26 | t.deepEqual( 27 | decamelizeKeys({fooBar: [['a', 'b']]}, {deep: true}), 28 | {'foo_bar': [['a', 'b']]}, 29 | ); 30 | }); 31 | 32 | test('accepts an array of objects', t => { 33 | t.deepEqual( 34 | decamelizeKeys([{fooBar: true}, {barFoo: false}, {'bar_foo': 'false'}]), 35 | [{'foo_bar': true}, {'bar_foo': false}, {'bar_foo': 'false'}], 36 | ); 37 | }); 38 | 39 | test('handle array of non-objects', t => { 40 | const input = ['name 1', 'name 2']; 41 | t.deepEqual( 42 | decamelizeKeys(input), 43 | input, 44 | ); 45 | }); 46 | 47 | test('handle array of non-objects with `deep` option', t => { 48 | const input = ['name 1', 'name 2']; 49 | t.deepEqual( 50 | decamelizeKeys(input, {deep: true}), 51 | input, 52 | ); 53 | }); 54 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # decamelize-keys 2 | 3 | > Convert object keys from camel case using [`decamelize`](https://github.com/sindresorhus/decamelize) 4 | 5 | ## Install 6 | 7 | ```sh 8 | npm install decamelize-keys 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```js 14 | import decamelizeKeys from 'decamelize-keys'; 15 | 16 | // Convert an object 17 | decamelizeKeys({fooBar: true}); 18 | //=> {foo_bar: true} 19 | 20 | // Convert an array of objects 21 | decamelizeKeys([{fooBar: true}, {barFoo: false}]); 22 | //=> [{foo_bar: true}, {bar_foo: false}] 23 | ``` 24 | 25 | ## API 26 | 27 | ### decamelizeKeys(input, options?) 28 | 29 | #### input 30 | 31 | Type: `object | object[]` 32 | 33 | An object or array of objects to decamelize. 34 | 35 | #### options 36 | 37 | Type: `object` 38 | 39 | ##### separator 40 | 41 | Type: `string`\ 42 | Default: `'_'` 43 | 44 | The character or string used to separate words. 45 | 46 | ```js 47 | import decamelizeKeys from 'decamelize-keys'; 48 | 49 | decamelizeKeys({fooBar: true}); 50 | //=> {foo_bar: true} 51 | 52 | decamelizeKeys({fooBar: true}, {separator: '-'}); 53 | //=> {'foo-bar': true} 54 | ``` 55 | 56 | ##### exclude 57 | 58 | Type: `Array`\ 59 | Default: `[]` 60 | 61 | Exclude keys from being decamelized. 62 | 63 | ##### deep 64 | 65 | Type: `boolean`\ 66 | Default: `false` 67 | 68 | Recurse nested objects and objects in arrays. 69 | 70 | ```js 71 | import decamelizeKeys from 'decamelize-keys'; 72 | 73 | decamelizeKeys({fooBar: true, nested: {unicornRainbow: true}}, {deep: true}); 74 | //=> {foo_bar: true, nested: {unicorn_rainbow: true}} 75 | ``` 76 | 77 | ## Related 78 | 79 | - [camelcase-keys](https://github.com/sindresorhus/camelcase-keys) - The inverse of this package. 80 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import mapObject from 'map-obj'; 2 | import QuickLru from 'quick-lru'; 3 | import decamelize from 'decamelize'; 4 | 5 | const has = (array, key) => array.some(element => { 6 | if (typeof element === 'string') { 7 | return element === key; 8 | } 9 | 10 | element.lastIndex = 0; 11 | 12 | return element.test(key); 13 | }); 14 | 15 | const cache = new QuickLru({maxSize: 100_000}); 16 | 17 | // Reproduces behavior from `map-obj`. 18 | const isObject = value => 19 | typeof value === 'object' 20 | && value !== null 21 | && !(value instanceof RegExp) 22 | && !(value instanceof Error) 23 | && !(value instanceof Date); 24 | 25 | const transform = (input, options = {}) => { 26 | if (!isObject(input)) { 27 | return input; 28 | } 29 | 30 | const { 31 | separator = '_', 32 | exclude, 33 | deep = false, 34 | } = options; 35 | 36 | const makeMapper = parentPath => (key, value) => { 37 | if (deep && isObject(value)) { 38 | const path = parentPath === undefined ? key : `${parentPath}.${key}`; 39 | value = mapObject(value, makeMapper(path)); 40 | } 41 | 42 | if (!(exclude && has(exclude, key))) { 43 | const cacheKey = `${separator}${key}`; 44 | 45 | if (cache.has(cacheKey)) { 46 | key = cache.get(cacheKey); 47 | } else { 48 | const returnValue = decamelize(key, {separator}); 49 | 50 | if (key.length < 100) { // Prevent abuse 51 | cache.set(cacheKey, returnValue); 52 | } 53 | 54 | key = returnValue; 55 | } 56 | } 57 | 58 | return [key, value]; 59 | }; 60 | 61 | return mapObject(input, makeMapper(undefined)); 62 | }; 63 | 64 | export default function decamelizeKeys(input, options) { 65 | if (Array.isArray(input)) { 66 | return Object.keys(input).map(key => transform(input[key], options)); 67 | } 68 | 69 | return transform(input, options); 70 | } 71 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import type {DelimiterCase} from 'type-fest'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/ban-types 4 | type EmptyTuple = []; 5 | 6 | /** 7 | Return a default type if input type is nil. 8 | 9 | @template T - Input type. 10 | @template U - Default type. 11 | */ 12 | type WithDefault = T extends undefined | void | null ? U : T; // eslint-disable-line @typescript-eslint/ban-types 13 | 14 | // TODO: Replace this with https://github.com/sindresorhus/type-fest/blob/main/source/includes.d.ts 15 | /** 16 | Check if an element is included in a tuple. 17 | */ 18 | type IsInclude = List extends undefined 19 | ? false 20 | : List extends Readonly 21 | ? false 22 | : List extends readonly [infer First, ...infer Rest] 23 | ? First extends Target 24 | ? true 25 | : IsInclude 26 | : boolean; 27 | 28 | /** 29 | Convert the keys of an object from camel case. 30 | */ 31 | export type DecamelizeKeys< 32 | T extends Record | readonly any[], 33 | Separator extends string = '_', 34 | Exclude extends readonly unknown[] = EmptyTuple, 35 | Deep extends boolean = false, 36 | > = T extends readonly any[] 37 | // Handle arrays or tuples. 38 | ? { 39 | [P in keyof T]: T[P] extends Record | readonly any[] 40 | // eslint-disable-next-line @typescript-eslint/ban-types 41 | ? {} extends DecamelizeKeys 42 | ? T[P] 43 | : DecamelizeKeys< 44 | T[P], 45 | Separator, 46 | Exclude, 47 | Deep 48 | > 49 | : T[P]; 50 | } 51 | : T extends Record 52 | // Handle objects. 53 | ? { 54 | [ 55 | P in keyof T as [IsInclude] extends [true] 56 | ? P 57 | : DelimiterCase 58 | ]: Record extends DecamelizeKeys 59 | ? T[P] 60 | : [Deep] extends [true] 61 | ? DecamelizeKeys< 62 | T[P], 63 | Separator, 64 | Exclude, 65 | Deep 66 | > 67 | : T[P]; 68 | } 69 | // Return anything else as-is. 70 | : T; 71 | 72 | type Options = { 73 | /** 74 | The character or string used to separate words. 75 | 76 | Important: You must use `as const` on the value. 77 | 78 | @default '_' 79 | 80 | @example 81 | ``` 82 | import decamelizeKeys from 'decamelize-keys'; 83 | 84 | decamelizeKeys({fooBar: true}); 85 | //=> {foo_bar: true} 86 | 87 | decamelizeKeys({fooBar: true}, {separator: '-' as const}); 88 | //=> {'foo-bar': true} 89 | ``` 90 | */ 91 | readonly separator?: Separator; 92 | 93 | /** 94 | Exclude keys from being camel-cased. 95 | 96 | If this option can be statically determined, it's recommended to add `as const` to it. 97 | 98 | @default [] 99 | */ 100 | readonly exclude?: ReadonlyArray; 101 | 102 | /** 103 | Recurse nested objects and objects in arrays. 104 | 105 | @default false 106 | 107 | @example 108 | ``` 109 | import decamelizeKeys from 'decamelize-keys'; 110 | 111 | decamelizeKeys({fooBar: true, nested: {unicornRainbow: true}}, {deep: true}); 112 | //=> {foo_bar: true, nested: {unicorn_rainbow: true}} 113 | ``` 114 | */ 115 | readonly deep?: boolean; 116 | }; 117 | 118 | /** 119 | Convert object keys from camel case using [`decamelize`](https://github.com/sindresorhus/decamelize). 120 | 121 | @param input - Object or array of objects to decamelize. 122 | 123 | @example 124 | ``` 125 | import decamelizeKeys from 'decamelize-keys'; 126 | 127 | // Convert an object 128 | decamelizeKeys({fooBar: true}); 129 | //=> {foo_bar: true} 130 | 131 | // Convert an array of objects 132 | decamelizeKeys([{fooBar: true}, {barFoo: false}]); 133 | //=> [{foo_bar: true}, {bar_foo: false}] 134 | ``` 135 | */ 136 | export default function decamelizeKeys< 137 | T extends Record | readonly any[], 138 | Separator extends string = '_', 139 | OptionsType extends Options = Options, 140 | >( 141 | input: T, 142 | options?: Options 143 | ): DecamelizeKeys< 144 | T, 145 | Separator, 146 | WithDefault, 147 | WithDefault 148 | >; 149 | --------------------------------------------------------------------------------