deep-case-crafter 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/readme.md CHANGED
@@ -1,7 +1,5 @@
1
1
  # DeepCaseCrafter
2
2
 
3
- **DeepCaseCrafter** transforms deeply nested object keys between case formats (`camelCase`, `PascalCase`, `snake_case`, `kebab-case`). It works on objects, arrays, Maps, and Sets, with configurable recursion depth, and types the result precisely: every key is typed the way it is transformed at runtime (with a few [exceptions](#result-types)).
4
-
5
3
  [![npm version](https://img.shields.io/npm/v/deep-case-crafter)](https://www.npmjs.com/package/deep-case-crafter)
6
4
  [![npm bundle size](https://img.shields.io/bundlephobia/minzip/deep-case-crafter)](https://bundlephobia.com/package/deep-case-crafter)
7
5
  [![npm type definitions](https://img.shields.io/npm/types/deep-case-crafter)](https://www.npmjs.com/package/deep-case-crafter)
@@ -10,15 +8,38 @@
10
8
  [![Maintenance](https://img.shields.io/maintenance/yes/2026.svg)](https://github.com/gyrgy/deep-case-crafter/graphs/commit-activity)
11
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/gyrgy/deep-case-crafter/blob/main/LICENSE)
12
10
 
11
+ Converts the keys of nested objects, arrays, Maps and Sets between camelCase,
12
+ snake_case, kebab-case and PascalCase, and types the result exactly.
13
+
14
+ ```typescript
15
+ import { transform } from 'deep-case-crafter';
16
+
17
+ // A record from a snake_case API
18
+ const apiUser = { user_id: 42, first_name: 'Ann', orders: [{ order_id: 7 }] };
19
+
20
+ const user = transform(apiUser);
21
+ // ^? { userId: number; firstName: string; orders: { orderId: number }[] }
22
+ console.log(user); // { userId: 42, firstName: 'Ann', orders: [{ orderId: 7 }] }
23
+
24
+ const body = transform(user, { targetCase: 'snake' });
25
+ // ^? { user_id: number; first_name: string; orders: { order_id: number }[] }
26
+ ```
27
+
13
28
  ## Features
14
29
 
15
- - Converts keys to `camelCase`, `PascalCase`, `snake_case`, and `kebab-case`.
16
- - **Acronym-aware**: `HTTPResponse` → `http_response` or `httpResponse`, `userID` → `user_id` — not `h_t_t_p_...` or `hTTPResponse`.
17
- - Works with deeply nested structures, including objects, arrays, Maps, and Sets.
18
- - Leaves `Date`, `RegExp`, `Error`, typed arrays, `Buffer` and other built-in values untouched.
19
- - **Precise result types**: every key is typed the way it is transformed at runtime, so `result.firstName` is known at compile time without casts.
20
- - Preserves special keys (`__`, `--`, leading numbers, special characters, words in capitals).
21
- - Configurable recursion depth (default: fully recursive).
30
+ - **Exact result types**: every key is typed the way it is converted at runtime, so
31
+ `user.firstName` is checked at compile time, without casts or type arguments (with a
32
+ few [exceptions](#result-types)).
33
+ - **No source case to declare**: every key's case is detected from the key itself, so
34
+ keys in different cases can be mixed in one input.
35
+ - **Acronym-aware**: `userID` → `user_id`, `HTTPResponse` → `http_response` or
36
+ `httpResponse`.
37
+ - **Deep**: objects, arrays, `Map` and `Set`, down to an optional `depth`, circular
38
+ references included. `Date`, `RegExp`, typed arrays and other built-in values are
39
+ returned untouched.
40
+ - **Keys that are data keep their names**: UUIDs always, and any other key whose path
41
+ you list in [`exclude`](#leaving-keys-unchanged), such as `'users_by_id.*'`.
42
+ - No dependencies, with ES module and CommonJS builds.
22
43
 
23
44
  ## Installation
24
45
 
@@ -28,15 +49,9 @@ npm install deep-case-crafter
28
49
 
29
50
  The type definitions require TypeScript 4.7 or later.
30
51
 
31
- > **⚠️ v2 breaking change:** `transform` is now a **named export**. Update
32
- > `import transform from 'deep-case-crafter'` to
33
- > `import { transform } from 'deep-case-crafter'` (CommonJS:
34
- > `const { transform } = require('deep-case-crafter')`). This fixes type
35
- > resolution for modern (`node16`/`nodenext`/bundler) consumers.
36
-
37
- > **⚠️ v3 breaking changes:** result types are precise without `sourceCase`, a few keys
38
- > convert differently, built-in values are returned untouched, and invalid options
39
- > throw. See [Upgrading to v3](#upgrading-to-v3).
52
+ Upgrading from an earlier major version? The
53
+ [changelog](https://github.com/gyrgy/deep-case-crafter/blob/main/CHANGELOG.md) lists
54
+ what changed in each version and what to update.
40
55
 
41
56
  ---
42
57
 
@@ -147,6 +162,49 @@ console.log(transform(strings, { targetCase: 'camel' }));
147
162
 
148
163
  ---
149
164
 
165
+ ## How it compares
166
+
167
+ Checked in October 2026 against
168
+ [ts-case-convert](https://www.npmjs.com/package/ts-case-convert) 2.3.1 (the closest
169
+ typed alternative), [camelcase-keys](https://www.npmjs.com/package/camelcase-keys)
170
+ 10.0.3, [snakecase-keys](https://www.npmjs.com/package/snakecase-keys) 9.0.2 and
171
+ [es-toolkit](https://www.npmjs.com/package/es-toolkit) 1.53.0 (`toCamelCaseKeys`,
172
+ `toSnakeCaseKeys`). Plain snake_case keys such as `user_id` become the same camelCase
173
+ keys in all of them. The table shows where they differ:
174
+
175
+ | | deep-case-crafter | ts-case-convert | camelcase-keys, snakecase-keys | es-toolkit |
176
+ | -------------------------------------------------- | --------------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------- |
177
+ | `userID` to snake_case | `user_id` | `user_i_d` | `user_id` | `user_id`, typed `user_i_d` |
178
+ | `HTTPResponse` to snake_case | `http_response` | `h_t_t_p_response` | `http_response` | `http_response`, typed `h_t_t_p_response` |
179
+ | `isAValidUser` to snake_case | `is_a_valid_user` | `is_a_valid_user` | `is_a_valid_user` | `is_a_valid_user` |
180
+ | `addressLine1` to snake_case | `address_line1` | `address_line_1`, or `address_line1` with `no-split-numbers` | `address_line1` | `address_line_1`, typed `address_line1` |
181
+ | Objects inside a `Map` or `Set` | keys converted | outside its documented scope | outside their documented scope | left as they are, as documented |
182
+ | Result type for `await res.json()`, an `any` input | `any` | a union that includes `Date` and `null`, so `.userId` does not compile | `any` | `any` |
183
+ | A `readonly string[]` field in the result type | `readonly string[]` | an object type whose `.map()` does not compile | `readonly string[]` | an object type: `.map()` compiles, `[0]` and `for...of` do not |
184
+ | A `[number, number]` field in the result type | `[number, number]` | `number[]` | `[number, number]` | `number[]` |
185
+ | Leaving chosen keys unchanged | `exclude` paths, such as `'metadata.*'` | no option | `exclude` keys by name or RegExp; camelcase-keys also `stopPaths` | no option |
186
+ | Target cases | camel, snake, kebab, Pascal | camel, snake, Pascal, SCREAMING_SNAKE | camel and Pascal; snake | camel, snake, kebab, Pascal, SCREAMING_SNAKE |
187
+
188
+ Where the others are better or different:
189
+
190
+ - **Bundle size**: bundled and minified with esbuild, ts-case-convert's `objectToCamel`
191
+ and `objectToSnake` take about 1.1 KB gzipped, deep-case-crafter's `transform` about
192
+ 2.6 KB.
193
+ - **More output**: ts-case-convert and es-toolkit also convert to SCREAMING_SNAKE_CASE,
194
+ and both have functions for single strings: `toSnake('userId')` and
195
+ `snakeCase('userId')` return `'user_id'`. deep-case-crafter has neither.
196
+ - **Digits**: ts-case-convert separates digits by default (`addressLine1` →
197
+ `address_line_1`), so `address_line_1` → `addressLine1` → `address_line_1`
198
+ round-trips. Its `ts-case-convert/no-split-numbers` entry point keeps digits attached
199
+ instead, as deep-case-crafter always does: `addressLine1` → `address_line1`.
200
+ - **Acronyms**: ts-case-convert splits every capital, so `userID` → `user_i_d` comes back
201
+ as `userID`. deep-case-crafter's `user_id` comes back as `userId`.
202
+ - **Options**: camelcase-keys and snakecase-keys leave keys unchanged by name or RegExp
203
+ at any depth, and snakecase-keys accepts your own conversion function. camelcase-keys
204
+ converts only the top level unless you pass `deep: true`.
205
+
206
+ ---
207
+
150
208
  ## Acronym-aware conversion
151
209
 
152
210
  When converting **to** `snake_case` or `kebab-case`, runs of capitals (acronyms)
@@ -161,13 +219,29 @@ console.log(result);
161
219
  // (not { get_h_t_t_p_response, user_i_d, ... })
162
220
  ```
163
221
 
222
+ The last capital before a lowercase letter starts the next word, so a single capital
223
+ before a capitalised word is a one-letter word, in every target case:
224
+
225
+ ```typescript
226
+ const input = { isAValidUser: 1, XAxis: 2 };
227
+
228
+ console.log(transform(input, { targetCase: 'snake' }));
229
+ // Output: { is_a_valid_user: 1, x_axis: 2 }
230
+
231
+ console.log(transform(input, { targetCase: 'camel' }));
232
+ // Output: { isAValidUser: 1, xAxis: 2 }
233
+ ```
234
+
235
+ Such keys round-trip: `three_d_secure` → `threeDSecure` → `three_d_secure`. A key that
236
+ starts with two capitals before a lowercase letter is read the same way:
237
+ `OAuthToken` → `o_auth_token`, `IPv4` → `i_pv4`.
238
+
164
239
  A plural `s` stays with its acronym: `allowedURLs` → `allowed_urls`,
165
- `getURLsForIDs` → `get_urls_for_ids`. An `s` word that follows an acronym directly is
166
- read the same way, so `SSLIsEnabled` becomes `sslis_enabled`.
240
+ `getURLsForIDs` → `get_urls_for_ids`, `IDs` → `ids`. An `s` word that follows an
241
+ acronym directly is read the same way, so `SSLIsEnabled` becomes `sslis_enabled`.
167
242
 
168
- When converting `PascalCase` **to** `camelCase`, a leading acronym is lowercased as
169
- a whole. Two capitals followed by a lowercase letter are a one-letter word and the next
170
- word, unless that letter is a plural `s` ending the acronym:
243
+ When converting `PascalCase` **to** `camelCase`, the first word is lowercased: a leading
244
+ acronym as a whole, a leading one-letter word on its own:
171
245
 
172
246
  ```typescript
173
247
  const input = { HTTPResponse: 1, UserID: 2, XAxis: 3, IDs: 4 };
@@ -199,22 +273,69 @@ The result **types** match all of this, so `result.get_http_response` and
199
273
  - **options**:
200
274
  - `targetCase` (optional): `'camel' | 'snake' | 'pascal' | 'kebab'` (default: `'camel'`, also used for any falsy value such as `''`, `null` or `0`). Any other value throws a `TypeError`.
201
275
  - `depth` (optional): **How many nesting levels to transform** (default: `Infinity` — the whole structure; `null` means the same). `1` transforms only the top-level keys, `2` the keys one level down as well, and so on. The values of objects, Maps and Sets are one level deeper; array items stay on their array's level. A value that is not a number (`NaN`, `'3'`, `3n`) throws a `TypeError`.
202
- - `sourceCase` (**deprecated**): has no effect. Every key's case is detected from the key itself, at runtime and in the result type. It is still accepted, and so is the 2.x form with explicit type arguments, `transform<Data, 'snake', 'camel'>(data, { sourceCase: 'snake', targetCase: 'camel' })`, except together with `depth` (leave the type arguments out to use `depth`); the key types follow the runtime, not `sourceCase` (see [Upgrading to v3](#upgrading-to-v3)).
276
+ - `exclude` (optional): **Keys to leave unchanged**, as paths of keys joined with dots, such as `['users_by_id.*', 'metadata.*']` (default: none; `null` means the same). See [Leaving keys unchanged](#leaving-keys-unchanged). Anything but an array of such paths throws a `TypeError`.
277
+ - `sourceCase` (**deprecated**): has no effect. Every key's case is detected from the key itself, at runtime and in the result type. It is still accepted, and so is the 2.x form with explicit type arguments, `transform<Data, 'snake', 'camel'>(data, { sourceCase: 'snake', targetCase: 'camel' })`, except together with `depth` or `exclude` (leave the type arguments out to use them); the key types follow the runtime, not `sourceCase` (see [3.0.0 in the changelog](https://github.com/gyrgy/deep-case-crafter/blob/main/CHANGELOG.md#300---2026-10-25)).
203
278
 
204
279
  Circular references are resolved to the transformed object. With a finite `depth`, a
205
280
  cycle is transformed once per level down to the limit instead: the result grows with the
206
281
  depth, and a depth of more than about a thousand levels on cyclic data exceeds the call
207
282
  stack and throws a `RangeError`. Omit `depth` for cyclic data, or keep it small.
208
283
 
284
+ ### Leaving keys unchanged
285
+
286
+ Keys that are data rather than field names, such as the IDs of a record, the keys of
287
+ free-form metadata, locale codes or Map keys, should keep their names. List their paths
288
+ in `exclude`:
289
+
290
+ ```typescript
291
+ const response = {
292
+ users_by_id: { abc_1: { first_name: 'Ann' } },
293
+ metadata: { order_ref: 'A1' },
294
+ };
295
+
296
+ const result = transform(response, {
297
+ exclude: ['users_by_id.*', 'metadata.*'],
298
+ });
299
+
300
+ console.log(result);
301
+ // Output: { usersById: { abc_1: { firstName: 'Ann' } }, metadata: { order_ref: 'A1' } }
302
+ ```
303
+
304
+ - A path is the chain of keys that leads to a key in the input (the keys before they are
305
+ transformed), joined with `.`. A `*` in place of a key matches any key.
306
+ - A key keeps its name when its whole path matches: `metadata.*` keeps the keys of
307
+ `metadata`, not `metadata` itself, nor the keys further down. The value of a kept key
308
+ is still transformed, so `first_name` above becomes `firstName`.
309
+ - Array items and Set members add no key to a path: `orders.order_id` matches the
310
+ `order_id` of every item of an `orders` array. Map keys do: `prices.*` keeps the keys
311
+ of a `prices` Map, and `*` also matches Map keys that are not strings.
312
+ - A key that contains a `.` cannot be named in a path, but `*` matches it.
313
+ - `exclude` does not change `depth`: keys below the depth limit are left unchanged
314
+ anyway. An empty array is the same as no `exclude`.
315
+
316
+ The result type follows the paths when they are written inline or `as const`; paths only
317
+ known as `string[]` give an `unknown` result type.
318
+
319
+ ### Passing `transform` as a callback
320
+
321
+ Wrap it in a function: `.then((data) => transform(data))`,
322
+ `rows.map((row) => transform(row))`. Passed by name, `transform` gets an `unknown` result
323
+ type, because TypeScript then uses its last overload, `transform<Input, Output>`, whose
324
+ output it cannot infer, and `map` would pass each row's index as the options.
325
+
209
326
  ### Result types
210
327
 
211
328
  The result type follows the runtime: every key is typed the way it is transformed
212
329
  (including the [preservation rules](#key-preservation-rules)), untouched values such as
213
330
  `Date` keep their type, and `readonly` arrays, tuples, Maps and Sets keep their shape. Use
214
- `TransformResult<Data, TargetCase, Depth>` to name it.
331
+ `TransformResult<Data, TargetCase, Depth, Exclude>` to name it, where `Exclude` is the
332
+ type of the `exclude` paths, such as `['metadata.*']`.
215
333
 
216
334
  - A literal `depth` is applied to the type precisely. A depth only known as a `number` (a
217
335
  variable, or `Infinity`) gives `unknown`, so omit `depth` to transform everything.
336
+ - `exclude` paths are applied to the type when they are literals, written inline or
337
+ stored `as const`. Paths only known as `string[]` give `unknown`, as a depth only known
338
+ as a `number` does.
218
339
  - `targetCase` and `depth` may be string enum members, branded literals, unions, or come
219
340
  from a stored options object: every options value they may hold gives its own result.
220
341
  Options that are not known at all give `unknown`: an options object that may be
@@ -268,6 +389,9 @@ A key is left **completely unchanged** when it:
268
389
  - **ends with a non-alphanumeric character**, e.g. `value_`, `count-`;
269
390
  - **contains characters outside** `A-Z a-z 0-9 _ -`, e.g. `user.name`, `a@b`;
270
391
  - **contains consecutive `__` or `--`**, e.g. `a__b`, `x--y` (ambiguous to detect);
392
+ - **is a UUID** (8-4-4-4-12 hexadecimal digits, in either case), e.g.
393
+ `f47ac10b-58cc-4372-a567-0e02b2c3d479`: it is data, not a name, so it is kept whether
394
+ it starts with a digit or a letter;
271
395
  - **is not written in one recognisable case**, e.g. `mixed_Case`, `USER_ID`, `a_b-c`, or
272
396
  is a word of two or more capitals, e.g. `ID`, `USD`, `B2B`.
273
397
 
@@ -275,7 +399,9 @@ Trailing digits are kept but treated as part of the word, so they do **not** by
275
399
  themselves preserve a key — `address_1` becomes `address1`, and the single word
276
400
  `item2` stays `item2`.
277
401
 
278
- These rules apply to the result types as well.
402
+ These rules apply to `Map` keys and to the result types as well. To keep other keys
403
+ unchanged, such as IDs or the keys of free-form metadata, list their paths in
404
+ [`exclude`](#leaving-keys-unchanged).
279
405
 
280
406
  ---
281
407
 
@@ -302,62 +428,28 @@ Worth knowing:
302
428
  - With `depth`, values below the limit are returned as they are: the result shares them
303
429
  with the input instead of copying them. An object reached at several levels is
304
430
  transformed once per level.
431
+ - With `exclude`, an object reached through several keys is transformed once for each
432
+ set of paths that match below them: one result when the same paths continue below
433
+ every key, one per set otherwise.
305
434
  - The transformation is recursive: data nested about a thousand levels deep or more can
306
435
  exceed the JavaScript call stack and throw an error (usually a `RangeError`).
307
436
 
308
437
  ---
309
438
 
310
- ## Upgrading to v3
311
-
312
- Runtime behaviour:
313
-
314
- - A few keys convert differently:
315
- - a leading acronym is lowercased as a whole in camelCase: `HTTPResponse` →
316
- `httpResponse` (was `hTTPResponse`), `IOStream` → `ioStream`, `URLs` → `urls`;
317
- - a plural `s` stays with its acronym: `allowedURLs` → snake_case `allowed_urls` (was
318
- `allowed_ur_ls`), `IDs` → camelCase `ids` (was `iDs`). An `s` word right after an
319
- acronym is read as a plural too: `SSLIsEnabled` → `sslis_enabled` (was
320
- `ssl_is_enabled`);
321
- - a key named `Infinity` is converted like any other word (`infinity`).
322
- - `Date`, `RegExp`, `Error`, `URL`, typed arrays, `Buffer` and other built-ins, objects
323
- with a `Symbol.toStringTag` (their own or their class's), `arguments` objects and module
324
- namespaces are returned untouched. 2.x rebuilt them as plain objects, which turned a
325
- `Date` into `{}`.
326
- - Maps and Sets from another realm (an iframe, `node:vm`) are transformed, and a Proxy
327
- around a Map or Set is returned untouched (2.x threw).
328
- - Enumerable properties added to `Object.prototype` are no longer copied into results.
329
- - Properties are read and transformed one at a time, so getters run in a different order,
330
- and Maps are read through their iterator rather than `entries()`.
331
- - With a finite `depth`, an object reached at several levels is transformed once per
332
- level, and a circular reference is transformed down to the limit (see the API section
333
- for large depths). 2.x shared one result, which depended on key order and ignored the
334
- limit.
335
- - An unsupported `targetCase`, or a `depth` that is not a number, throws a `TypeError`.
336
- - Very deeply nested data reaches the call stack limit at different depths: nested
337
- objects can go deeper than in 2.x, chains of Maps or Sets nested more than about 1,400
338
- levels deep no longer fit.
339
-
340
- Types:
341
-
342
- - The result type is precise without `sourceCase`, which is deprecated and has no effect.
343
- `TransformResult` is exported to name it.
344
- - Assigning or casting (`as Foo`) the result of `transform(x)` to a declared type now
345
- checks it against the real result type; a cast to an unrelated type fails with TS2352.
346
- If `x` is typed `unknown` or loosely, state the output type:
347
- `transform<Input, Output>(x)`.
348
- - `depth: Infinity` (or any `depth` not known as a literal) gives `unknown`: omit `depth`
349
- instead.
350
- - An input typed `any` gives `any` (2.x gave `unknown`).
351
- - Key types follow the runtime, also when `sourceCase` is passed, so code that relied on
352
- 2.x's key types for keys the runtime converts differently (`result.hTTPResponse`) no
353
- longer compiles.
354
- - `TransformOptions` includes `depth`, so `Required<TransformOptions>` requires it.
355
- - The 2.x form with three type arguments, `transform<Data, 'snake', 'camel'>(...)`, no
356
- longer accepts `depth`: leave the type arguments out instead. As an instantiation
357
- expression (`ReturnType<typeof transform<Data, 'snake', 'camel'>>`) it no longer
358
- compiles: use `TransformResult<Data, 'camel'>`.
359
- - `Parameters<typeof transform>[1]` (the options of the last overload) no longer includes
360
- the deprecated `sourceCase`.
439
+ ## Versioning
440
+
441
+ deep-case-crafter follows [semantic versioning](https://semver.org). The v3 API is
442
+ stable. Key outputs change only to fix bugs, and every change is listed in the
443
+ [changelog](https://github.com/gyrgy/deep-case-crafter/blob/main/CHANGELOG.md): if you
444
+ store converted keys, such as database columns, check it before upgrading. A minor
445
+ version can add an option to `TransformOptions`, which `Required<TransformOptions>` then
446
+ requires.
447
+
448
+ ### Upgrading to v3
449
+
450
+ The changes from 2.x, and what to update, are listed under
451
+ [3.0.0](https://github.com/gyrgy/deep-case-crafter/blob/main/CHANGELOG.md#300---2026-10-25)
452
+ in the changelog.
361
453
 
362
454
  ---
363
455
 
@@ -382,4 +474,6 @@ MIT
382
474
 
383
475
  - [NPM Package](https://www.npmjs.com/package/deep-case-crafter)
384
476
  - [GitHub Repository](https://github.com/gyrgy/deep-case-crafter)
477
+ - [Changelog](https://github.com/gyrgy/deep-case-crafter/blob/main/CHANGELOG.md)
478
+ - [Security Policy](https://github.com/gyrgy/deep-case-crafter/blob/main/SECURITY.md)
385
479
  - [TypeScript Docs](https://www.typescriptlang.org/)