deep-case-crafter 3.0.0 → 3.2.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/dist/index.d.cts +287 -45
- package/dist/index.d.mts +287 -45
- package/dist/index.d.ts +287 -45
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +20 -21
- package/readme.md +222 -77
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deep-case-crafter",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.2.0",
|
|
4
|
+
"description": "Convert object keys between camelCase, snake_case, kebab-case and PascalCase, deeply (objects, arrays, Map, Set), with exact TypeScript types",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -32,27 +32,26 @@
|
|
|
32
32
|
"license": "MIT",
|
|
33
33
|
"private": false,
|
|
34
34
|
"keywords": [
|
|
35
|
+
"camelCase",
|
|
36
|
+
"snake_case",
|
|
37
|
+
"kebab-case",
|
|
38
|
+
"PascalCase",
|
|
35
39
|
"camelcase",
|
|
36
|
-
"pascalcase",
|
|
37
40
|
"snakecase",
|
|
38
41
|
"kebabcase",
|
|
42
|
+
"pascalcase",
|
|
43
|
+
"camel-case",
|
|
44
|
+
"snake-case",
|
|
45
|
+
"case-convert",
|
|
46
|
+
"convert-case",
|
|
39
47
|
"case-conversion",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"typescript",
|
|
44
|
-
"typescript-utils",
|
|
45
|
-
"deep-map",
|
|
48
|
+
"object-keys",
|
|
49
|
+
"convert-keys",
|
|
50
|
+
"transform-keys",
|
|
46
51
|
"map-keys",
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"case-format",
|
|
51
|
-
"deep-case",
|
|
52
|
-
"convert-case",
|
|
53
|
-
"string-utils",
|
|
54
|
-
"key-mapper",
|
|
55
|
-
"key-converter"
|
|
52
|
+
"deep",
|
|
53
|
+
"nested",
|
|
54
|
+
"typescript"
|
|
56
55
|
],
|
|
57
56
|
"scripts": {
|
|
58
57
|
"build": "rollup -c",
|
|
@@ -62,11 +61,11 @@
|
|
|
62
61
|
"test-coverage": "npm test -- --coverage",
|
|
63
62
|
"test:types": "tsc --noEmit",
|
|
64
63
|
"tsc": "tsc --noEmit",
|
|
65
|
-
"test:dist-types": "tsc -p dist-type-tests && tsc -p dist-type-tests/tsconfig.declarations.json && tsc -p dist-type-tests/tsconfig.loose.json",
|
|
64
|
+
"test:dist-types": "tsc -p dist-type-tests && tsc -p dist-type-tests/tsconfig.declarations.json && node dist-type-tests/checkDeclarations.mjs && tsc -p dist-type-tests/tsconfig.consumer.json && tsc -p dist-type-tests/tsconfig.loose.json && tsc -p dist-type-tests/tsconfig.exact.json",
|
|
66
65
|
"lint": "eslint --ext .js,.jsx,.ts,.tsx src/ --max-warnings=0 --no-cache",
|
|
67
66
|
"lint:fix": "eslint --ext .js,.jsx,.ts,.tsx src/ --fix --max-warnings=0 --no-cache",
|
|
68
|
-
"format": "prettier --write src dist-type-tests benchmark .github readme.md",
|
|
69
|
-
"format:check": "prettier --check src dist-type-tests benchmark .github readme.md",
|
|
67
|
+
"format": "prettier --write src dist-type-tests benchmark .github readme.md CHANGELOG.md SECURITY.md",
|
|
68
|
+
"format:check": "prettier --check src dist-type-tests benchmark .github readme.md CHANGELOG.md SECURITY.md",
|
|
70
69
|
"benchmark": "tsx benchmark/*.benchmark.ts",
|
|
71
70
|
"check": "npm run lint && npm run format:check && npm run test:types && npm run test-coverage && npm run build && npm run test:dist-types"
|
|
72
71
|
},
|
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
|
[](https://www.npmjs.com/package/deep-case-crafter)
|
|
6
4
|
[](https://bundlephobia.com/package/deep-case-crafter)
|
|
7
5
|
[](https://www.npmjs.com/package/deep-case-crafter)
|
|
@@ -10,15 +8,41 @@
|
|
|
10
8
|
[](https://github.com/gyrgy/deep-case-crafter/graphs/commit-activity)
|
|
11
9
|
[](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
|
-
-
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
|
|
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
|
+
- **Single keys too**: [`transformKey`](#transformkeykey-options) converts one key by
|
|
43
|
+
the same rules, `transformKey('userID', { targetCase: 'snake' })` → `'user_id'`, typed
|
|
44
|
+
`'user_id'`.
|
|
45
|
+
- No dependencies, with ES module and CommonJS builds.
|
|
22
46
|
|
|
23
47
|
## Installation
|
|
24
48
|
|
|
@@ -28,15 +52,9 @@ npm install deep-case-crafter
|
|
|
28
52
|
|
|
29
53
|
The type definitions require TypeScript 4.7 or later.
|
|
30
54
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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).
|
|
55
|
+
Upgrading from an earlier major version? The
|
|
56
|
+
[changelog](https://github.com/gyrgy/deep-case-crafter/blob/main/CHANGELOG.md) lists
|
|
57
|
+
what changed in each version and what to update.
|
|
40
58
|
|
|
41
59
|
---
|
|
42
60
|
|
|
@@ -147,6 +165,70 @@ console.log(transform(strings, { targetCase: 'camel' }));
|
|
|
147
165
|
|
|
148
166
|
---
|
|
149
167
|
|
|
168
|
+
### 7. Convert a single key with `transformKey`
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { transformKey } from 'deep-case-crafter';
|
|
172
|
+
|
|
173
|
+
transformKey('user_id'); // 'userId'
|
|
174
|
+
transformKey('userID', { targetCase: 'snake' }); // 'user_id'
|
|
175
|
+
|
|
176
|
+
type SortField = 'createdAt' | 'userID';
|
|
177
|
+
|
|
178
|
+
function toSortParam(field: SortField) {
|
|
179
|
+
return transformKey(field, { targetCase: 'snake' });
|
|
180
|
+
// ^? 'created_at' | 'user_id'
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## How it compares
|
|
187
|
+
|
|
188
|
+
Checked in October 2026 against
|
|
189
|
+
[ts-case-convert](https://www.npmjs.com/package/ts-case-convert) 2.3.1 (the closest
|
|
190
|
+
typed alternative), [camelcase-keys](https://www.npmjs.com/package/camelcase-keys)
|
|
191
|
+
10.0.3, [snakecase-keys](https://www.npmjs.com/package/snakecase-keys) 9.0.2 and
|
|
192
|
+
[es-toolkit](https://www.npmjs.com/package/es-toolkit) 1.53.0 (`toCamelCaseKeys`,
|
|
193
|
+
`toSnakeCaseKeys`). Plain snake_case keys such as `user_id` become the same camelCase
|
|
194
|
+
keys in all of them. The table shows where they differ:
|
|
195
|
+
|
|
196
|
+
| | deep-case-crafter | ts-case-convert | camelcase-keys, snakecase-keys | es-toolkit |
|
|
197
|
+
| -------------------------------------------------- | --------------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
198
|
+
| `userID` to snake_case | `user_id` | `user_i_d` | `user_id` | `user_id`, typed `user_i_d` |
|
|
199
|
+
| `HTTPResponse` to snake_case | `http_response` | `h_t_t_p_response` | `http_response` | `http_response`, typed `h_t_t_p_response` |
|
|
200
|
+
| `isAValidUser` to snake_case | `is_a_valid_user` | `is_a_valid_user` | `is_a_valid_user` | `is_a_valid_user` |
|
|
201
|
+
| `addressLine1` to snake_case | `address_line1` | `address_line_1`, or `address_line1` with `no-split-numbers` | `address_line1` | `address_line_1`, typed `address_line1` |
|
|
202
|
+
| Objects inside a `Map` or `Set` | keys converted | outside its documented scope | outside their documented scope | left as they are, as documented |
|
|
203
|
+
| Result type for `await res.json()`, an `any` input | `any` | a union that includes `Date` and `null`, so `.userId` does not compile | `any` | `any` |
|
|
204
|
+
| 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 |
|
|
205
|
+
| A `[number, number]` field in the result type | `[number, number]` | `number[]` | `[number, number]` | `number[]` |
|
|
206
|
+
| Leaving chosen keys unchanged | `exclude` paths, such as `'metadata.*'` | no option | `exclude` keys by name or RegExp; camelcase-keys also `stopPaths` | no option |
|
|
207
|
+
| Target cases | camel, snake, kebab, Pascal | camel, snake, Pascal, SCREAMING_SNAKE | camel and Pascal; snake | camel, snake, kebab, Pascal, SCREAMING_SNAKE |
|
|
208
|
+
|
|
209
|
+
Where the others are better or different:
|
|
210
|
+
|
|
211
|
+
- **Bundle size**: bundled and minified with esbuild, ts-case-convert's `objectToCamel`
|
|
212
|
+
and `objectToSnake` take about 1.1 KB gzipped, deep-case-crafter's `transform` about
|
|
213
|
+
2.6 KB.
|
|
214
|
+
- **More output**: ts-case-convert and es-toolkit also convert to SCREAMING_SNAKE_CASE,
|
|
215
|
+
which deep-case-crafter does not.
|
|
216
|
+
- **Single strings**: like `transformKey`, ts-case-convert's `toSnake('userId')` and
|
|
217
|
+
es-toolkit's `snakeCase('userId')` return `'user_id'`. es-toolkit's functions also
|
|
218
|
+
convert text, such as `'Hello World'` → `'hello_world'`, which `transformKey` returns
|
|
219
|
+
unchanged.
|
|
220
|
+
- **Digits**: ts-case-convert separates digits by default (`addressLine1` →
|
|
221
|
+
`address_line_1`), so `address_line_1` → `addressLine1` → `address_line_1`
|
|
222
|
+
round-trips. Its `ts-case-convert/no-split-numbers` entry point keeps digits attached
|
|
223
|
+
instead, as deep-case-crafter always does: `addressLine1` → `address_line1`.
|
|
224
|
+
- **Acronyms**: ts-case-convert splits every capital, so `userID` → `user_i_d` comes back
|
|
225
|
+
as `userID`. deep-case-crafter's `user_id` comes back as `userId`.
|
|
226
|
+
- **Options**: camelcase-keys and snakecase-keys leave keys unchanged by name or RegExp
|
|
227
|
+
at any depth, and snakecase-keys accepts your own conversion function. camelcase-keys
|
|
228
|
+
converts only the top level unless you pass `deep: true`.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
150
232
|
## Acronym-aware conversion
|
|
151
233
|
|
|
152
234
|
When converting **to** `snake_case` or `kebab-case`, runs of capitals (acronyms)
|
|
@@ -161,13 +243,29 @@ console.log(result);
|
|
|
161
243
|
// (not { get_h_t_t_p_response, user_i_d, ... })
|
|
162
244
|
```
|
|
163
245
|
|
|
246
|
+
The last capital before a lowercase letter starts the next word, so a single capital
|
|
247
|
+
before a capitalised word is a one-letter word, in every target case:
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
const input = { isAValidUser: 1, XAxis: 2 };
|
|
251
|
+
|
|
252
|
+
console.log(transform(input, { targetCase: 'snake' }));
|
|
253
|
+
// Output: { is_a_valid_user: 1, x_axis: 2 }
|
|
254
|
+
|
|
255
|
+
console.log(transform(input, { targetCase: 'camel' }));
|
|
256
|
+
// Output: { isAValidUser: 1, xAxis: 2 }
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Such keys round-trip: `three_d_secure` → `threeDSecure` → `three_d_secure`. A key that
|
|
260
|
+
starts with two capitals before a lowercase letter is read the same way:
|
|
261
|
+
`OAuthToken` → `o_auth_token`, `IPv4` → `i_pv4`.
|
|
262
|
+
|
|
164
263
|
A plural `s` stays with its acronym: `allowedURLs` → `allowed_urls`,
|
|
165
|
-
`getURLsForIDs` → `get_urls_for_ids`. An `s` word that follows an
|
|
166
|
-
read the same way, so `SSLIsEnabled` becomes `sslis_enabled`.
|
|
264
|
+
`getURLsForIDs` → `get_urls_for_ids`, `IDs` → `ids`. An `s` word that follows an
|
|
265
|
+
acronym directly is read the same way, so `SSLIsEnabled` becomes `sslis_enabled`.
|
|
167
266
|
|
|
168
|
-
When converting `PascalCase` **to** `camelCase`,
|
|
169
|
-
a whole
|
|
170
|
-
word, unless that letter is a plural `s` ending the acronym:
|
|
267
|
+
When converting `PascalCase` **to** `camelCase`, the first word is lowercased: a leading
|
|
268
|
+
acronym as a whole, a leading one-letter word on its own:
|
|
171
269
|
|
|
172
270
|
```typescript
|
|
173
271
|
const input = { HTTPResponse: 1, UserID: 2, XAxis: 3, IDs: 4 };
|
|
@@ -199,22 +297,69 @@ The result **types** match all of this, so `result.get_http_response` and
|
|
|
199
297
|
- **options**:
|
|
200
298
|
- `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
299
|
- `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
|
-
- `
|
|
300
|
+
- `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`.
|
|
301
|
+
- `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
302
|
|
|
204
303
|
Circular references are resolved to the transformed object. With a finite `depth`, a
|
|
205
304
|
cycle is transformed once per level down to the limit instead: the result grows with the
|
|
206
305
|
depth, and a depth of more than about a thousand levels on cyclic data exceeds the call
|
|
207
306
|
stack and throws a `RangeError`. Omit `depth` for cyclic data, or keep it small.
|
|
208
307
|
|
|
308
|
+
### Leaving keys unchanged
|
|
309
|
+
|
|
310
|
+
Keys that are data rather than field names, such as the IDs of a record, the keys of
|
|
311
|
+
free-form metadata, locale codes or Map keys, should keep their names. List their paths
|
|
312
|
+
in `exclude`:
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
const response = {
|
|
316
|
+
users_by_id: { abc_1: { first_name: 'Ann' } },
|
|
317
|
+
metadata: { order_ref: 'A1' },
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const result = transform(response, {
|
|
321
|
+
exclude: ['users_by_id.*', 'metadata.*'],
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
console.log(result);
|
|
325
|
+
// Output: { usersById: { abc_1: { firstName: 'Ann' } }, metadata: { order_ref: 'A1' } }
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
- A path is the chain of keys that leads to a key in the input (the keys before they are
|
|
329
|
+
transformed), joined with `.`. A `*` in place of a key matches any key.
|
|
330
|
+
- A key keeps its name when its whole path matches: `metadata.*` keeps the keys of
|
|
331
|
+
`metadata`, not `metadata` itself, nor the keys further down. The value of a kept key
|
|
332
|
+
is still transformed, so `first_name` above becomes `firstName`.
|
|
333
|
+
- Array items and Set members add no key to a path: `orders.order_id` matches the
|
|
334
|
+
`order_id` of every item of an `orders` array. Map keys do: `prices.*` keeps the keys
|
|
335
|
+
of a `prices` Map, and `*` also matches Map keys that are not strings.
|
|
336
|
+
- A key that contains a `.` cannot be named in a path, but `*` matches it.
|
|
337
|
+
- `exclude` does not change `depth`: keys below the depth limit are left unchanged
|
|
338
|
+
anyway. An empty array is the same as no `exclude`.
|
|
339
|
+
|
|
340
|
+
The result type follows the paths when they are written inline or `as const`; paths only
|
|
341
|
+
known as `string[]` give an `unknown` result type.
|
|
342
|
+
|
|
343
|
+
### Passing `transform` as a callback
|
|
344
|
+
|
|
345
|
+
Wrap it in a function: `.then((data) => transform(data))`,
|
|
346
|
+
`rows.map((row) => transform(row))`. Passed by name, `transform` gets an `unknown` result
|
|
347
|
+
type, because TypeScript then uses its last overload, `transform<Input, Output>`, whose
|
|
348
|
+
output it cannot infer, and `map` would pass each row's index as the options.
|
|
349
|
+
|
|
209
350
|
### Result types
|
|
210
351
|
|
|
211
352
|
The result type follows the runtime: every key is typed the way it is transformed
|
|
212
353
|
(including the [preservation rules](#key-preservation-rules)), untouched values such as
|
|
213
354
|
`Date` keep their type, and `readonly` arrays, tuples, Maps and Sets keep their shape. Use
|
|
214
|
-
`TransformResult<Data, TargetCase, Depth>` to name it
|
|
355
|
+
`TransformResult<Data, TargetCase, Depth, Exclude>` to name it, where `Exclude` is the
|
|
356
|
+
type of the `exclude` paths, such as `['metadata.*']`.
|
|
215
357
|
|
|
216
358
|
- A literal `depth` is applied to the type precisely. A depth only known as a `number` (a
|
|
217
359
|
variable, or `Infinity`) gives `unknown`, so omit `depth` to transform everything.
|
|
360
|
+
- `exclude` paths are applied to the type when they are literals, written inline or
|
|
361
|
+
stored `as const`. Paths only known as `string[]` give `unknown`, as a depth only known
|
|
362
|
+
as a `number` does.
|
|
218
363
|
- `targetCase` and `depth` may be string enum members, branded literals, unions, or come
|
|
219
364
|
from a stored options object: every options value they may hold gives its own result.
|
|
220
365
|
Options that are not known at all give `unknown`: an options object that may be
|
|
@@ -237,6 +382,33 @@ The result type follows the runtime: every key is typed the way it is transforme
|
|
|
237
382
|
untouched when it has a `Symbol.toStringTag` (as every class implementing `Map` or
|
|
238
383
|
`Set` does), and rebuilt as a plain object otherwise.
|
|
239
384
|
|
|
385
|
+
### `transformKey(key, options)`
|
|
386
|
+
|
|
387
|
+
Converts a single key by the same rules as the keys of `transform`'s result.
|
|
388
|
+
|
|
389
|
+
- **key**: The key to transform, a string. Anything else throws a `TypeError`.
|
|
390
|
+
- **options**:
|
|
391
|
+
- `targetCase` (optional): as for `transform` (default: `'camel'`). Pass it in an
|
|
392
|
+
object: `transformKey(key, 'snake')` throws a `TypeError`.
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
transformKey('HTTPResponse', { targetCase: 'snake' }); // 'http_response'
|
|
396
|
+
transformKey('Hello World', { targetCase: 'snake' }); // 'Hello World'
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
It converts names, not text: a string that is not a key in one of the four cases is
|
|
400
|
+
returned unchanged, such as `'Hello World'`, `'USER_ID'`, `'_id'` or a UUID (see
|
|
401
|
+
[Key Preservation Rules](#key-preservation-rules)).
|
|
402
|
+
|
|
403
|
+
The result type is the converted key. A string literal gives a literal, a union of keys
|
|
404
|
+
or of target cases gives the union of their results, and a `string` gives `string`.
|
|
405
|
+
`TransformKey<Key, TargetCase>` names it: `TransformKey<'userID', 'snake'>` is
|
|
406
|
+
`'user_id'`. Options that are not known exactly, such as an options object that may be
|
|
407
|
+
`undefined`, give a wider type: `string`, or the key in any of the four cases.
|
|
408
|
+
|
|
409
|
+
To use it in a callback, wrap it in a function:
|
|
410
|
+
`keys.map((key) => transformKey(key, { targetCase: 'snake' }))`.
|
|
411
|
+
|
|
240
412
|
---
|
|
241
413
|
|
|
242
414
|
## Advanced: explicit output type overload
|
|
@@ -268,6 +440,9 @@ A key is left **completely unchanged** when it:
|
|
|
268
440
|
- **ends with a non-alphanumeric character**, e.g. `value_`, `count-`;
|
|
269
441
|
- **contains characters outside** `A-Z a-z 0-9 _ -`, e.g. `user.name`, `a@b`;
|
|
270
442
|
- **contains consecutive `__` or `--`**, e.g. `a__b`, `x--y` (ambiguous to detect);
|
|
443
|
+
- **is a UUID** (8-4-4-4-12 hexadecimal digits, in either case), e.g.
|
|
444
|
+
`f47ac10b-58cc-4372-a567-0e02b2c3d479`: it is data, not a name, so it is kept whether
|
|
445
|
+
it starts with a digit or a letter;
|
|
271
446
|
- **is not written in one recognisable case**, e.g. `mixed_Case`, `USER_ID`, `a_b-c`, or
|
|
272
447
|
is a word of two or more capitals, e.g. `ID`, `USD`, `B2B`.
|
|
273
448
|
|
|
@@ -275,7 +450,9 @@ Trailing digits are kept but treated as part of the word, so they do **not** by
|
|
|
275
450
|
themselves preserve a key — `address_1` becomes `address1`, and the single word
|
|
276
451
|
`item2` stays `item2`.
|
|
277
452
|
|
|
278
|
-
These rules apply to the result types as well.
|
|
453
|
+
These rules apply to `Map` keys, to `transformKey` and to the result types as well. To
|
|
454
|
+
keep other keys unchanged, such as IDs or the keys of free-form metadata, list their
|
|
455
|
+
paths in [`exclude`](#leaving-keys-unchanged).
|
|
279
456
|
|
|
280
457
|
---
|
|
281
458
|
|
|
@@ -302,62 +479,28 @@ Worth knowing:
|
|
|
302
479
|
- With `depth`, values below the limit are returned as they are: the result shares them
|
|
303
480
|
with the input instead of copying them. An object reached at several levels is
|
|
304
481
|
transformed once per level.
|
|
482
|
+
- With `exclude`, an object reached through several keys is transformed once for each
|
|
483
|
+
set of paths that match below them: one result when the same paths continue below
|
|
484
|
+
every key, one per set otherwise.
|
|
305
485
|
- The transformation is recursive: data nested about a thousand levels deep or more can
|
|
306
486
|
exceed the JavaScript call stack and throw an error (usually a `RangeError`).
|
|
307
487
|
|
|
308
488
|
---
|
|
309
489
|
|
|
310
|
-
##
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
-
|
|
323
|
-
|
|
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`.
|
|
490
|
+
## Versioning
|
|
491
|
+
|
|
492
|
+
deep-case-crafter follows [semantic versioning](https://semver.org). The v3 API is
|
|
493
|
+
stable. Key outputs change only to fix bugs, and every change is listed in the
|
|
494
|
+
[changelog](https://github.com/gyrgy/deep-case-crafter/blob/main/CHANGELOG.md): if you
|
|
495
|
+
store converted keys, such as database columns, check it before upgrading. A minor
|
|
496
|
+
version can add an option, which `Required<TransformOptions>` and
|
|
497
|
+
`Required<TransformKeyOptions>` then require.
|
|
498
|
+
|
|
499
|
+
### Upgrading to v3
|
|
500
|
+
|
|
501
|
+
The changes from 2.x, and what to update, are listed under
|
|
502
|
+
[3.0.0](https://github.com/gyrgy/deep-case-crafter/blob/main/CHANGELOG.md#300---2026-10-25)
|
|
503
|
+
in the changelog.
|
|
361
504
|
|
|
362
505
|
---
|
|
363
506
|
|
|
@@ -382,4 +525,6 @@ MIT
|
|
|
382
525
|
|
|
383
526
|
- [NPM Package](https://www.npmjs.com/package/deep-case-crafter)
|
|
384
527
|
- [GitHub Repository](https://github.com/gyrgy/deep-case-crafter)
|
|
528
|
+
- [Changelog](https://github.com/gyrgy/deep-case-crafter/blob/main/CHANGELOG.md)
|
|
529
|
+
- [Security Policy](https://github.com/gyrgy/deep-case-crafter/blob/main/SECURITY.md)
|
|
385
530
|
- [TypeScript Docs](https://www.typescriptlang.org/)
|