deep-case-crafter
v2.1.1
Published
Transforms deeply nested object, array, Map, and Set keys between common case formats while preserving TypeScript type safety
Maintainers
Readme
DeepCaseCrafter
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.
Features
- Converts keys to
camelCase,PascalCase,snake_case, andkebab-case. - Acronym-aware:
HTTPResponse→http_response,userID→user_id— noth_t_t_p_.... - Works with deeply nested structures, including objects, arrays, Maps, and Sets.
- Precise TypeScript inference when
sourceCaseandtargetCaseare set (otherwise the result isunknown). - Preserves special keys (
__,--, leading numbers, special characters). - Configurable recursion depth (default: fully recursive).
Installation
npm install deep-case-crafter⚠️ v2 breaking change:
transformis now a named export. Updateimport transform from 'deep-case-crafter'toimport { transform } from 'deep-case-crafter'(CommonJS:const { transform } = require('deep-case-crafter')). This fixes type resolution for modern (node16/nodenext/bundler) consumers.
Quick Start
1. Basic usage — just call transform
import { transform } from 'deep-case-crafter';
const input = { user_id: 1, first_name: 'John' };
const result = transform(input);
console.log(result);
// Output: { userId: 1, firstName: 'John' }(Automatically converts from snake_case to camelCase)
2. Specify the targetCase
const input = { user_id: 1, first_name: 'John' };
const pascalCaseResult = transform(input, { targetCase: 'pascal' });
console.log(pascalCaseResult);
// Output: { UserId: 1, FirstName: 'John' }
const kebabCaseResult = transform(input, { targetCase: 'kebab' });
console.log(kebabCaseResult);
// Output: { "user-id": 1, "first-name": "John" }3. Explicitly define sourceCase for TypeScript benefits
TypeScript infers the exact output keys when both sourceCase and targetCase are set.
const input = { user_id: 1, first_name: 'John' };
const result = transform(input, { targetCase: 'camel', sourceCase: 'snake' });
console.log(result);
// Output: { userId: 1, firstName: 'John' }4. Transform deeply nested structures
const nestedInput = {
user_info: { first_name: 'John', last_name: 'Doe' },
posts: [{ post_id: 1, post_title: 'Hello' }],
};
const transformed = transform(nestedInput, {
targetCase: 'camel',
sourceCase: 'snake',
});
console.log(transformed);
// Output:
// {
// userInfo: { firstName: 'John', lastName: 'Doe' },
// posts: [{ postId: 1, postTitle: 'Hello' }]
// }5. Transforming a Map
const mapInput = new Map([
['user_id', 1],
['first_name', 'John'],
]);
const result = transform(mapInput, {
targetCase: 'camel',
sourceCase: 'snake',
});
console.log(result.get('userId')); // 1
console.log(result.get('firstName')); // 'John'6. Transforming a Set
A Set is a collection of values, not key/value pairs, so its primitive
members (strings, numbers, etc.) are passed through unchanged — only the keys
of objects nested inside the Set are transformed.
const setInput = new Set([{ user_id: 1 }, { first_name: 'John' }]);
const result = transform(setInput, {
targetCase: 'camel',
sourceCase: 'snake',
});
console.log(result); // Set { { userId: 1 }, { firstName: 'John' } }
// Primitive members are left as-is:
const strings = new Set(['user_id', 'first_name']);
console.log(transform(strings, { targetCase: 'camel', sourceCase: 'snake' }));
// Set { 'user_id', 'first_name' }Acronym-aware conversion
When converting to snake_case or kebab-case, runs of capitals (acronyms)
are treated as whole words instead of being split letter-by-letter:
const input = { getHTTPResponse: 1, userID: 2, parseHTMLString: 3 };
const result = transform(input, { sourceCase: 'camel', targetCase: 'snake' });
console.log(result);
// Output: { get_http_response: 1, user_id: 2, parse_html_string: 3 }
// (not { get_h_t_t_p_response, user_i_d, ... })The output types match this exactly when sourceCase and targetCase are
set, so result.get_http_response is known at compile time.
API
transform(data, options)
- data: The data structure to transform (
object,array,Map, orSet). - options:
targetCase(optional):'camel' | 'snake' | 'pascal' | 'kebab'(default:'camel')sourceCase(optional):'snake' | 'camel' | 'pascal' | 'kebab'. Omit it to auto-detect each key's case at runtime.depth(optional): Recursion depth (default:Infinity— the whole structure is transformed). Pass a number to stop transforming below that level. Circular references are always handled safely.
TypeScript inference: precise output types are produced only when both
sourceCaseandtargetCaseare passed. Without both, the call still works at runtime (auto-detecting each key) but the result type isunknown.
sourceCaseis a type-level assertion, not a runtime switch. At runtime every key's case is auto-detected regardless ofsourceCase, so the precise types are accurate only for keys genuinely well-formed in the declared source case. Ambiguous keys — mixed case (mixed_Case),SCREAMING_SNAKE, or mixed separators (a_b-c) — are preserved unchanged at runtime, even though the type may show them transformed. SetsourceCaseonly when your keys really are that case. (Out-of-charset keys such ascafé_tableora.b_care preserved at both layers.)
Advanced: explicit output type overload
If you want to specify the output type explicitly (for example, when you know the exact shape you want), you can use the following overload:
const result = transform<Input, Output>(obj, { targetCase: 'camel' });
const typedResult: Output = result; // TypeScript will enforce Output type hereNote:
- When using this overload, you cannot pass
sourceCasein the options. If you do, TypeScript will show an error:Object literal may only specify known properties, and 'sourceCase' does not exist in type 'Omit<TransformOptions, "sourceCase"> & { depth?: number }'.
- 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.
- If you want to use
sourceCaseand benefit from type inference, use the standard overload:
const result = transform(obj, { targetCase: 'camel', sourceCase: 'snake' }); // Output type is inferredKey Preservation Rules
A key is left completely unchanged when it:
- starts with anything other than a letter (a digit or special character), e.g.
1st_place,_private; - ends with a non-alphanumeric character, e.g.
value_,count-; - contains characters outside
A-Z a-z 0-9 _ -, e.g.user.name,a@b; - contains consecutive
__or--, e.g.a__b,x--y(ambiguous to detect).
Trailing digits are kept but treated as part of the word, so they do not by
themselves preserve a key — address_1 becomes address1, and the single word
item2 stays item2.
Contributing
- Fork the repository.
- Create a new branch (
feature/my-feature). - Commit your changes.
- Submit a pull request.
License
MIT
