deep-case-crafter 2.1.1 → 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/dist/index.d.cts +397 -186
- package/dist/index.d.mts +397 -186
- package/dist/index.d.ts +397 -186
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +23 -21
- package/readme.md +288 -55
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 produces precise output types when both `sourceCase` and `targetCase` are set.
|
|
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,14 +8,38 @@
|
|
|
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
|
-
-
|
|
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.
|
|
21
43
|
|
|
22
44
|
## Installation
|
|
23
45
|
|
|
@@ -25,11 +47,11 @@
|
|
|
25
47
|
npm install deep-case-crafter
|
|
26
48
|
```
|
|
27
49
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
50
|
+
The type definitions require TypeScript 4.7 or later.
|
|
51
|
+
|
|
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.
|
|
33
55
|
|
|
34
56
|
---
|
|
35
57
|
|
|
@@ -42,12 +64,13 @@ import { transform } from 'deep-case-crafter';
|
|
|
42
64
|
|
|
43
65
|
const input = { user_id: 1, first_name: 'John' };
|
|
44
66
|
const result = transform(input);
|
|
67
|
+
// ^? { userId: number; firstName: string }
|
|
45
68
|
|
|
46
69
|
console.log(result);
|
|
47
70
|
// Output: { userId: 1, firstName: 'John' }
|
|
48
71
|
```
|
|
49
72
|
|
|
50
|
-
_(Automatically converts from `snake_case` to `camelCase
|
|
73
|
+
_(Automatically converts from `snake_case` to `camelCase`; the case of every key is detected from the key itself)_
|
|
51
74
|
|
|
52
75
|
---
|
|
53
76
|
|
|
@@ -67,16 +90,18 @@ console.log(kebabCaseResult);
|
|
|
67
90
|
|
|
68
91
|
---
|
|
69
92
|
|
|
70
|
-
### 3.
|
|
71
|
-
|
|
72
|
-
_TypeScript infers the exact output keys when **both** `sourceCase` and `targetCase` are set._
|
|
93
|
+
### 3. Name the result type with `TransformResult`
|
|
73
94
|
|
|
74
95
|
```typescript
|
|
75
|
-
|
|
76
|
-
const result = transform(input, { targetCase: 'camel', sourceCase: 'snake' });
|
|
96
|
+
import { transform, type TransformResult } from 'deep-case-crafter';
|
|
77
97
|
|
|
78
|
-
|
|
79
|
-
|
|
98
|
+
type ApiUser = { user_id: number; first_name: string; created_at: Date };
|
|
99
|
+
type User = TransformResult<ApiUser>;
|
|
100
|
+
// { userId: number; firstName: string; createdAt: Date }
|
|
101
|
+
|
|
102
|
+
function toUser(apiUser: ApiUser): User {
|
|
103
|
+
return transform(apiUser);
|
|
104
|
+
}
|
|
80
105
|
```
|
|
81
106
|
|
|
82
107
|
---
|
|
@@ -89,10 +114,7 @@ const nestedInput = {
|
|
|
89
114
|
posts: [{ post_id: 1, post_title: 'Hello' }],
|
|
90
115
|
};
|
|
91
116
|
|
|
92
|
-
const transformed = transform(nestedInput, {
|
|
93
|
-
targetCase: 'camel',
|
|
94
|
-
sourceCase: 'snake',
|
|
95
|
-
});
|
|
117
|
+
const transformed = transform(nestedInput, { targetCase: 'camel' });
|
|
96
118
|
|
|
97
119
|
console.log(transformed);
|
|
98
120
|
// Output:
|
|
@@ -107,15 +129,12 @@ console.log(transformed);
|
|
|
107
129
|
### 5. Transforming a `Map`
|
|
108
130
|
|
|
109
131
|
```typescript
|
|
110
|
-
const mapInput = new Map([
|
|
132
|
+
const mapInput = new Map<string, number | string>([
|
|
111
133
|
['user_id', 1],
|
|
112
134
|
['first_name', 'John'],
|
|
113
135
|
]);
|
|
114
136
|
|
|
115
|
-
const result = transform(mapInput, {
|
|
116
|
-
targetCase: 'camel',
|
|
117
|
-
sourceCase: 'snake',
|
|
118
|
-
});
|
|
137
|
+
const result = transform(mapInput, { targetCase: 'camel' });
|
|
119
138
|
|
|
120
139
|
console.log(result.get('userId')); // 1
|
|
121
140
|
console.log(result.get('firstName')); // 'John'
|
|
@@ -131,21 +150,61 @@ of objects nested inside the Set** are transformed.
|
|
|
131
150
|
|
|
132
151
|
```typescript
|
|
133
152
|
const setInput = new Set([{ user_id: 1 }, { first_name: 'John' }]);
|
|
134
|
-
const result = transform(setInput, {
|
|
135
|
-
targetCase: 'camel',
|
|
136
|
-
sourceCase: 'snake',
|
|
137
|
-
});
|
|
153
|
+
const result = transform(setInput, { targetCase: 'camel' });
|
|
138
154
|
|
|
139
155
|
console.log(result); // Set { { userId: 1 }, { firstName: 'John' } }
|
|
140
156
|
|
|
141
157
|
// Primitive members are left as-is:
|
|
142
158
|
const strings = new Set(['user_id', 'first_name']);
|
|
143
|
-
console.log(transform(strings, { targetCase: 'camel'
|
|
159
|
+
console.log(transform(strings, { targetCase: 'camel' }));
|
|
144
160
|
// Set { 'user_id', 'first_name' }
|
|
145
161
|
```
|
|
146
162
|
|
|
147
163
|
---
|
|
148
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
|
+
|
|
149
208
|
## Acronym-aware conversion
|
|
150
209
|
|
|
151
210
|
When converting **to** `snake_case` or `kebab-case`, runs of capitals (acronyms)
|
|
@@ -153,15 +212,56 @@ are treated as whole words instead of being split letter-by-letter:
|
|
|
153
212
|
|
|
154
213
|
```typescript
|
|
155
214
|
const input = { getHTTPResponse: 1, userID: 2, parseHTMLString: 3 };
|
|
156
|
-
const result = transform(input, {
|
|
215
|
+
const result = transform(input, { targetCase: 'snake' });
|
|
157
216
|
|
|
158
217
|
console.log(result);
|
|
159
218
|
// Output: { get_http_response: 1, user_id: 2, parse_html_string: 3 }
|
|
160
219
|
// (not { get_h_t_t_p_response, user_i_d, ... })
|
|
161
220
|
```
|
|
162
221
|
|
|
163
|
-
The
|
|
164
|
-
|
|
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
|
+
|
|
239
|
+
A plural `s` stays with its acronym: `allowedURLs` → `allowed_urls`,
|
|
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`.
|
|
242
|
+
|
|
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:
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
const input = { HTTPResponse: 1, UserID: 2, XAxis: 3, IDs: 4 };
|
|
248
|
+
const result = transform(input, { targetCase: 'camel' });
|
|
249
|
+
|
|
250
|
+
console.log(result);
|
|
251
|
+
// Output: { httpResponse: 1, userID: 2, xAxis: 3, ids: 4 }
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Words of two or more capitals, such as `ID`, `USD` or `GET`, are left unchanged, like
|
|
255
|
+
`SCREAMING_SNAKE` keys (see [Key Preservation Rules](#key-preservation-rules)). A single
|
|
256
|
+
capital, with or without digits (`S`, `S3`), is converted as a one-letter word.
|
|
257
|
+
|
|
258
|
+
Converting back to `PascalCase` cannot restore a lowercased acronym: `httpResponse`
|
|
259
|
+
becomes `HttpResponse`, and `ids` becomes `Ids`. In the other direction, a camelCase key
|
|
260
|
+
that starts with a one-letter word before an acronym does not come back either:
|
|
261
|
+
`iOSVersion` → `IOSVersion` → `iosVersion`.
|
|
262
|
+
|
|
263
|
+
The result **types** match all of this, so `result.get_http_response` and
|
|
264
|
+
`result.httpResponse` are known at compile time.
|
|
165
265
|
|
|
166
266
|
---
|
|
167
267
|
|
|
@@ -171,19 +271,92 @@ set, so `result.get_http_response` is known at compile time.
|
|
|
171
271
|
|
|
172
272
|
- **data**: The data structure to transform (`object`, `array`, `Map`, or `Set`).
|
|
173
273
|
- **options**:
|
|
174
|
-
- `targetCase` (optional): `'camel' | 'snake' | 'pascal' | 'kebab'` (default: `'camel'`)
|
|
175
|
-
- `
|
|
176
|
-
- `
|
|
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`.
|
|
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`.
|
|
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)).
|
|
278
|
+
|
|
279
|
+
Circular references are resolved to the transformed object. With a finite `depth`, a
|
|
280
|
+
cycle is transformed once per level down to the limit instead: the result grows with the
|
|
281
|
+
depth, and a depth of more than about a thousand levels on cyclic data exceeds the call
|
|
282
|
+
stack and throws a `RangeError`. Omit `depth` for cyclic data, or keep it small.
|
|
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
|
+
};
|
|
177
295
|
|
|
178
|
-
|
|
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
|
+
```
|
|
179
303
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
+
|
|
326
|
+
### Result types
|
|
327
|
+
|
|
328
|
+
The result type follows the runtime: every key is typed the way it is transformed
|
|
329
|
+
(including the [preservation rules](#key-preservation-rules)), untouched values such as
|
|
330
|
+
`Date` keep their type, and `readonly` arrays, tuples, Maps and Sets keep their shape. Use
|
|
331
|
+
`TransformResult<Data, TargetCase, Depth, Exclude>` to name it, where `Exclude` is the
|
|
332
|
+
type of the `exclude` paths, such as `['metadata.*']`.
|
|
333
|
+
|
|
334
|
+
- A literal `depth` is applied to the type precisely. A depth only known as a `number` (a
|
|
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.
|
|
339
|
+
- `targetCase` and `depth` may be string enum members, branded literals, unions, or come
|
|
340
|
+
from a stored options object: every options value they may hold gives its own result.
|
|
341
|
+
Options that are not known at all give `unknown`: an options object that may be
|
|
342
|
+
`undefined`, one typed as `TransformOptions` or `any`, one carrying other properties
|
|
343
|
+
(without `sourceCase`), and options passed with only the input type given
|
|
344
|
+
(`transform<Data>(data, options)`). Leave the type arguments out to get the precise
|
|
345
|
+
type.
|
|
346
|
+
- When a library exports a value or function built on `transform`, annotate it with
|
|
347
|
+
`TransformResult<...>`. Otherwise the result type is written out into its declaration
|
|
348
|
+
files: recursive types (trees, JSON values) are cut off as `any`, and very large types
|
|
349
|
+
or symbol-keyed members fail to compile (TS7056, TS4118). Generic functions around
|
|
350
|
+
`transform` are not affected.
|
|
351
|
+
- Some values cannot be told apart by their types. `Error`, `URL`, boxed primitives,
|
|
352
|
+
iterators, module namespace objects and other built-ins whose types do not declare a
|
|
353
|
+
`Symbol.toStringTag` are returned untouched, but typed as plain objects with
|
|
354
|
+
transformed keys. Class instances lose their prototype methods, getters and setters at
|
|
355
|
+
runtime, while their type still lists them. A class that implements `Map`, `Set`,
|
|
356
|
+
`ReadonlyMap` or `ReadonlySet` without extending `Map` or `Set` (and a Proxy around a
|
|
357
|
+
Map or Set) is typed as a transformed `Map` or `Set`, but at runtime it is returned
|
|
358
|
+
untouched when it has a `Symbol.toStringTag` (as every class implementing `Map` or
|
|
359
|
+
`Set` does), and rebuilt as a plain object otherwise.
|
|
187
360
|
|
|
188
361
|
---
|
|
189
362
|
|
|
@@ -198,13 +371,12 @@ const typedResult: Output = result; // TypeScript will enforce Output type here
|
|
|
198
371
|
|
|
199
372
|
**Note:**
|
|
200
373
|
|
|
201
|
-
- When using this overload, you cannot pass `sourceCase` in the options
|
|
202
|
-
> Object literal may only specify known properties, and 'sourceCase' does not exist in type 'Omit<TransformOptions, "sourceCase"> & { depth?: number }'.
|
|
374
|
+
- When using this overload, you cannot pass `sourceCase` in the options; TypeScript reports an error if you do.
|
|
203
375
|
- This overload is useful when you want to take full control of the output type, but type safety between input, options, and output is not enforced by the library.
|
|
204
|
-
- If you want
|
|
376
|
+
- If you want the result type inferred, use the standard overload:
|
|
205
377
|
|
|
206
378
|
```typescript
|
|
207
|
-
const result = transform(obj, { targetCase: 'camel'
|
|
379
|
+
const result = transform(obj, { targetCase: 'camel' }); // Output type is inferred
|
|
208
380
|
```
|
|
209
381
|
|
|
210
382
|
---
|
|
@@ -216,12 +388,69 @@ A key is left **completely unchanged** when it:
|
|
|
216
388
|
- **starts with anything other than a letter** (a digit or special character), e.g. `1st_place`, `_private`;
|
|
217
389
|
- **ends with a non-alphanumeric character**, e.g. `value_`, `count-`;
|
|
218
390
|
- **contains characters outside** `A-Z a-z 0-9 _ -`, e.g. `user.name`, `a@b`;
|
|
219
|
-
- **contains consecutive `__` or `--`**, e.g. `a__b`, `x--y` (ambiguous to detect)
|
|
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;
|
|
395
|
+
- **is not written in one recognisable case**, e.g. `mixed_Case`, `USER_ID`, `a_b-c`, or
|
|
396
|
+
is a word of two or more capitals, e.g. `ID`, `USD`, `B2B`.
|
|
220
397
|
|
|
221
398
|
Trailing digits are kept but treated as part of the word, so they do **not** by
|
|
222
399
|
themselves preserve a key — `address_1` becomes `address1`, and the single word
|
|
223
400
|
`item2` stays `item2`.
|
|
224
401
|
|
|
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).
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
## What Gets Transformed
|
|
409
|
+
|
|
410
|
+
| Value | Result |
|
|
411
|
+
| --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
412
|
+
| Plain objects, `Object.create(null)` objects | A new plain object with transformed keys |
|
|
413
|
+
| Class instances | A new plain object with their own enumerable properties (prototype methods are not copied) |
|
|
414
|
+
| Arrays | A new array with every item transformed |
|
|
415
|
+
| `Map`, including one from another realm | A new `Map` with string keys transformed and every value transformed |
|
|
416
|
+
| `Set`, including one from another realm | A new `Set` with every member transformed (primitive members are kept as-is) |
|
|
417
|
+
| Primitives, functions and built-ins such as `Date`, `RegExp`, `Error`, `URL`, `Promise`, typed arrays, `Buffer` | Returned untouched (the same reference) |
|
|
418
|
+
| Objects with a `Symbol.toStringTag` of their own or from their class, `arguments` objects, module namespaces, and Proxies around a Map or Set | Returned untouched (the same reference) |
|
|
419
|
+
|
|
420
|
+
Symbol-keyed properties are copied unchanged.
|
|
421
|
+
|
|
422
|
+
Worth knowing:
|
|
423
|
+
|
|
424
|
+
- If two keys of one object become the same key (`user_id` and `userId` both become
|
|
425
|
+
`userId`), the last one wins.
|
|
426
|
+
- Every property is read once, in order, and its value is transformed before the next one
|
|
427
|
+
is read; getters run during the transformation. Maps are read through their iterator.
|
|
428
|
+
- With `depth`, values below the limit are returned as they are: the result shares them
|
|
429
|
+
with the input instead of copying them. An object reached at several levels is
|
|
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.
|
|
434
|
+
- The transformation is recursive: data nested about a thousand levels deep or more can
|
|
435
|
+
exceed the JavaScript call stack and throw an error (usually a `RangeError`).
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
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.
|
|
453
|
+
|
|
225
454
|
---
|
|
226
455
|
|
|
227
456
|
## Contributing
|
|
@@ -229,7 +458,9 @@ themselves preserve a key — `address_1` becomes `address1`, and the single wor
|
|
|
229
458
|
1. Fork the repository.
|
|
230
459
|
2. Create a new branch (`feature/my-feature`).
|
|
231
460
|
3. Commit your changes.
|
|
232
|
-
4.
|
|
461
|
+
4. Run `npm run check` (lint, formatting, types, tests with coverage, build and the
|
|
462
|
+
published types).
|
|
463
|
+
5. Submit a pull request.
|
|
233
464
|
|
|
234
465
|
---
|
|
235
466
|
|
|
@@ -243,4 +474,6 @@ MIT
|
|
|
243
474
|
|
|
244
475
|
- [NPM Package](https://www.npmjs.com/package/deep-case-crafter)
|
|
245
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)
|
|
246
479
|
- [TypeScript Docs](https://www.typescriptlang.org/)
|