deep-case-crafter 2.1.1 → 3.0.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 CHANGED
@@ -1,7 +1,30 @@
1
+ /**
2
+ * `true` when `T` is `any`: a conditional type checked against `any` takes both branches,
3
+ * so only `any` turns `true | false` into `boolean`.
4
+ *
5
+ * Two shorter checks are not used. `0 extends 1 & T` is `false` since TypeScript 5.4 for
6
+ * a type parameter whose constraint excludes `1` (such as `To` in `transform`), even when
7
+ * `any` is passed for it. `unknown extends T` is `true` without `strictNullChecks` for an
8
+ * object type whose properties are all optional (such as `{ targetCase?: 'kebab' }`).
9
+ */
10
+ type IsAny<T> = boolean extends (T extends never ? true : false) ? true : false;
11
+ /**
12
+ * `true` when `S` is a single string literal such as `'user_id'`, and `false` for
13
+ * `string` and template-literal patterns such as `` `user_${string}` ``.
14
+ */
15
+ type IsStringLiteral<S extends string> = Record<never, never> extends Record<S, never> ? false : true;
16
+
1
17
  /**
2
18
  * Represents the supported string case formats for transformation
3
19
  */
4
20
  type StringCase = 'camel' | 'pascal' | 'snake' | 'kebab';
21
+ /**
22
+ * The plain literal of a target case: a string enum member or a branded literal such as
23
+ * `'snake' & { __brand: 'Case' }` becomes `'snake'`, so it compares equal to the case
24
+ * detected from a key. Only literals are produced, never `To` itself: TypeScript 4.7
25
+ * narrows `To` to `To & StringCase` in a true branch, which is `never` for an enum member.
26
+ */
27
+ type PlainCase<To> = To extends 'camel' ? 'camel' : To extends 'snake' ? 'snake' : To extends 'kebab' ? 'kebab' : To extends 'pascal' ? 'pascal' : never;
5
28
 
6
29
  /**
7
30
  * Options for the transform function
@@ -11,250 +34,280 @@ interface TransformOptions {
11
34
  * The target casing format to transform to
12
35
  * @default 'camel'
13
36
  */
14
- targetCase?: StringCase;
37
+ targetCase?: StringCase | undefined;
15
38
  /**
16
- * The source casing format.
17
- *
18
- * This is a **type-level assertion only**, used (together with `targetCase`) to
19
- * produce precise output types. It does **not** change runtime behavior: at runtime
20
- * every key's case is auto-detected individually whether or not `sourceCase` is set.
21
- *
22
- * Because of that, the precise types are accurate only for keys that are genuinely
23
- * well-formed in the declared source case. For ambiguous keys — mixed case
24
- * (`mixed_Case`), SCREAMING_SNAKE (`FOO_BAR`), mixed separators (`a_b-c`), or
25
- * non-ASCII (`café_table`) — the runtime preserves the key unchanged, while the type
26
- * may show it transformed. Set `sourceCase` only when your keys really are that case.
27
- *
28
- * When omitted, the result type is `unknown` (the runtime still auto-detects each key).
39
+ * How many nesting levels to transform: `1` transforms only the top-level keys, `2`
40
+ * also the keys one level down, and so on. The values of objects, Maps and Sets are one
41
+ * level deeper; array items stay on their array's level. Deeper values are returned
42
+ * as they are.
29
43
  *
30
- * @example
31
- * // Precise types when both are set and keys are well-formed snake_case:
32
- * const result = transform(data, { targetCase: 'camel', sourceCase: 'snake' });
44
+ * Pass a numeric literal to get a precise result type. A depth that is only known to be
45
+ * a `number` (a variable, or `Infinity`) gives an `unknown` result type; omit `depth`
46
+ * to transform every level.
33
47
  *
34
- * @default undefined
48
+ * @default Infinity
35
49
  */
36
- sourceCase?: StringCase;
50
+ depth?: number | undefined;
51
+ /**
52
+ * @deprecated Has no effect. Every key's case is detected from the key itself, both at
53
+ * runtime and in the result type, so there is nothing to declare.
54
+ */
55
+ sourceCase?: StringCase | undefined;
37
56
  }
38
-
39
57
  /**
40
- * Type utility that transforms a snake_case string to camelCase
41
- * Example: 'hello_world' -> 'helloWorld'
58
+ * Rejects option names `transform` does not know. The options object is inferred as a
59
+ * whole, so without this an object literal would accept any extra property, typos such
60
+ * as `depht` included. The error names the unknown option.
42
61
  */
43
- type SnakeToCamel<S extends string> = S extends `${infer P}_${infer C}${infer R}` ? `${P}${Uppercase<C>}${SnakeToCamel<R>}` : S;
62
+ type NoUnknownOptions<Options> = {
63
+ [K in Exclude<keyof Options, keyof TransformOptions>]: `Unknown option: ${K & string}`;
64
+ };
65
+ /** `To` as a plain case literal, or `'camel'` (the default) for `undefined` */
66
+ type CaseOrDefault<To> = To extends undefined ? 'camel' : PlainCase<To>;
44
67
  /**
45
- * Type utility that transforms a snake_case string to PascalCase
46
- * Example: 'hello_world' -> 'HelloWorld'
68
+ * The target case the runtime uses for one options object: `'camel'` when `targetCase`
69
+ * is omitted or `undefined`.
47
70
  */
48
- type SnakeWordsToPascal<Parts extends string[]> = Parts extends [
49
- infer First extends string,
50
- ...infer Rest extends string[]
51
- ] ? `${Capitalize<First>}${SnakeWordsToPascal<Rest>}` : '';
52
- type SnakeToWords<S extends string> = S extends `${infer P}_${infer R}` ? [P, ...SnakeToWords<R>] : [S];
53
- type SnakeToPascal<S extends string> = S extends `${infer P}_${infer R}` ? SnakeWordsToPascal<[P, ...SnakeToWords<R>]> : Capitalize<S>;
71
+ type TargetCaseOf<Options> = 'targetCase' extends keyof Options ? CaseOrDefault<Options['targetCase']> : 'camel';
54
72
  /**
55
- * Type utility that transforms a snake_case string to kebab-case
56
- * Example: 'hello_world' -> 'hello-world'
73
+ * The depth the runtime uses for one options object: `undefined` (every level) when
74
+ * `depth` is omitted.
57
75
  */
58
- type SnakeToKebab<S extends string> = S extends `${infer P}_${infer R}` ? `${P}-${SnakeToKebab<R>}` : S;
76
+ type DepthOf<Options> = 'depth' extends keyof Options ? Options['depth'] : undefined;
77
+
78
+ /**
79
+ * Character classes used by the type-level key transformation. They spell out the
80
+ * ASCII characters explicitly, because TypeScript's `Uppercase`/`Lowercase` are
81
+ * Unicode-aware and would treat a letter like `é` as an ordinary letter.
82
+ */
83
+ /** Lowercase ASCII letters */
84
+ type LowerLetter = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';
85
+ /** Uppercase ASCII letters */
86
+ type UpperLetter = Uppercase<LowerLetter>;
87
+ /** ASCII digits */
88
+ type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
89
+ /** The word separators of snake_case and kebab-case */
90
+ type Separator = '_' | '-';
91
+ /** Every character a transformable key may contain (the runtime's `A-Z a-z 0-9 _ -`) */
92
+ type KeyChar = LowerLetter | UpperLetter | Digit | Separator;
93
+
94
+ /**
95
+ * The plural suffix that stays with the acronym before it instead of starting a new
96
+ * word: `allowedURLs` becomes `allowed_urls`, not `allowed_ur_ls`.
97
+ */
98
+ declare const ACRONYM_PLURAL_SUFFIX = "s";
99
+
59
100
  /**
60
- * Acronym-aware word-boundary scanner shared by all camel/pascal → snake/kebab
61
- * conversions. Mirrors `utils/stringFormatters/toSeparatedCase.ts` exactly so the
62
- * type-level and runtime results never diverge.
101
+ * Type-level mirror of the runtime key transformation (`transformCase`).
63
102
  *
64
- * Inserts `Sep` before an uppercase char when the previous char is a lowercase
65
- * letter or digit, or when it ends an acronym run that starts a new word (the two
66
- * preceding chars are uppercase and the next is lowercase). All output is lowercased.
103
+ * Every step below follows a runtime function, so the type of a transformed key is
104
+ * exactly the key the runtime produces. The parity test (`typeRuntimeParity.test.ts`)
105
+ * checks this for thousands of keys.
67
106
  */
68
- type IsUpper<C extends string> = C extends Uppercase<C> ? (C extends Lowercase<C> ? false : true) : false;
69
- type IsLower<C extends string> = C extends Lowercase<C> ? (C extends Uppercase<C> ? false : true) : false;
70
- type IsLowerOrDigit<C extends string> = IsLower<C> extends true ? true : C extends `${number}` ? true : false;
71
- type FirstChar<S extends string> = S extends `${infer F}${string}` ? F : '';
72
107
  /**
73
- * Whether an uppercase `C` at the current position begins a new word (so a
74
- * separator goes before it): true when the previous char is lower/digit, or when
75
- * `C` ends an acronym run (two preceding uppercase chars) followed by a lowercase.
108
+ * `true` when every character of `S` may appear in a transformable key
109
+ * (tail-recursive, so long keys compile).
76
110
  */
77
- type NeedsSeparator<C extends string, Prev extends string, PrevPrev extends string, Next extends string> = IsUpper<C> extends true ? IsLowerOrDigit<Prev> extends true ? true : IsUpper<Prev> extends true ? IsUpper<PrevPrev> extends true ? IsLower<Next> : false : false : false;
78
- type SeparatorScan<S extends string, Sep extends string, Prev extends string = '', PrevPrev extends string = '', Acc extends string = ''> = S extends `${infer C}${infer R}` ? SeparatorScan<R, Sep, C, Prev, `${Acc}${NeedsSeparator<C, Prev, PrevPrev, FirstChar<R>> extends true ? Sep : ''}${Lowercase<C>}`> : Acc;
79
- type SeparatorCase<S extends string, Sep extends string> = SeparatorScan<S, Sep>;
111
+ type HasOnlyKeyChars<S extends string> = S extends `${infer C}${infer Rest}` ? C extends KeyChar ? HasOnlyKeyChars<Rest> : false : true;
80
112
  /**
81
- * Type utility that transforms a camelCase string to snake_case (acronym-aware)
82
- * Example: 'helloWorld' -> 'hello_world', 'getHTTPResponse' -> 'get_http_response'
113
+ * Keys the runtime leaves unchanged (`prepareStringTransformation`): keys containing a
114
+ * character outside `A-Z a-z 0-9 _ -`, keys not starting with a letter and keys ending
115
+ * with a separator.
83
116
  */
84
- type CamelToSnake<S extends string> = SeparatorCase<S, '_'>;
117
+ type IsPreservedKey<K extends string> = HasOnlyKeyChars<K> extends false ? true : K extends `${LowerLetter | UpperLetter}${string}` ? K extends `${string}${Separator}` ? true : false : true;
85
118
  /**
86
- * Type utility that transforms a camelCase string to PascalCase
87
- * Example: 'helloWorld' -> 'HelloWorld'
119
+ * The case the runtime detects for a key that is not preserved (`detectStringCase`), or
120
+ * `'none'` when it cannot be detected (the key is then left unchanged). A string is used
121
+ * rather than `null`, which every type accepts without `strictNullChecks`.
88
122
  */
89
- type CamelToPascal<S extends string> = Capitalize<S>;
123
+ type DetectedCase<K extends string> = K extends `${string}${Separator}${string}` ? K extends Lowercase<K> ? K extends `${string}-${string}` ? K extends `${string}_${string}` | `${string}--${string}` ? 'none' : 'kebab' : K extends `${string}__${string}` ? 'none' : 'snake' : 'none' : K extends `${infer First}${infer Rest}` ? Rest extends Lowercase<Rest> ? 'single' : First extends LowerLetter ? 'camel' : K extends Uppercase<K> ? 'none' : 'pascal' : 'none';
124
+ /** The first character of `S`, or `''` */
125
+ type FirstChar<S extends string> = S extends `${infer C}${string}` ? C : '';
90
126
  /**
91
- * Type utility that transforms a camelCase string to kebab-case (acronym-aware)
92
- * Example: 'helloWorld' -> 'hello-world', 'getHTTPResponse' -> 'get-http-response'
127
+ * Whether a new word starts at `C`, followed by `Rest` (`isWordBoundary`): `C` is
128
+ * uppercase and follows a lowercase letter or digit, or it ends an acronym that starts a
129
+ * new word (two uppercase letters before it, a lowercase letter after it) — unless that
130
+ * letter is a plural `s` not followed by another lowercase letter.
93
131
  */
94
- type CamelToKebab<S extends string> = SeparatorCase<S, '-'>;
132
+ type IsWordBoundary<C extends string, Prev extends string, PrevPrev extends string, Rest extends string> = C extends UpperLetter ? Prev extends LowerLetter | Digit ? true : Prev extends UpperLetter ? PrevPrev extends UpperLetter ? Rest extends `${infer Next}${infer AfterNext}` ? Next extends LowerLetter ? Next extends typeof ACRONYM_PLURAL_SUFFIX ? FirstChar<AfterNext> extends LowerLetter ? true : false : true : false : false : false : false : false;
133
+ /** camelCase/PascalCase to snake_case or kebab-case, acronym-aware (`toSeparatedCase`) */
134
+ type ToSeparatedCase<S extends string, Sep extends Separator, Prev extends string = '', PrevPrev extends string = '', Acc extends string = ''> = S extends `${infer C}${infer Rest}` ? ToSeparatedCase<Rest, Sep, C, Prev, `${Acc}${IsWordBoundary<C, Prev, PrevPrev, Rest> extends true ? Sep : ''}${Lowercase<C>}`> : Acc;
95
135
  /**
96
- * Type utility that transforms a PascalCase string to camelCase
97
- * Example: 'HelloWorld' -> 'helloWorld'
136
+ * Lowercases the first word, up to the first word boundary, so a leading acronym is
137
+ * lowercased as a whole
98
138
  */
99
- type PascalToCamel<S extends string> = S extends `${infer F}${infer R}` ? `${Lowercase<F>}${R}` : S;
139
+ type LowercaseFirstWord<S extends string, Prev extends string = '', PrevPrev extends string = '', Acc extends string = ''> = S extends `${infer C}${infer Rest}` ? IsWordBoundary<C, Prev, PrevPrev, Rest> extends true ? `${Acc}${S}` : LowercaseFirstWord<Rest, C, Prev, `${Acc}${Lowercase<C>}`> : Acc;
100
140
  /**
101
- * Type utility that transforms a PascalCase string to snake_case (acronym-aware)
102
- * Example: 'HelloWorld' -> 'hello_world', 'HTTPResponse' -> 'http_response'
141
+ * PascalCase to camelCase (`pascalToCamelCase`): two capitals followed by a lowercase
142
+ * letter start with a one-letter word (`XAxis` -> `xAxis`), unless that letter is a
143
+ * plural `s` ending the acronym (`IDs` -> `ids`); otherwise the first word is lowercased
144
+ * as a whole (`HTTPRequest` -> `httpRequest`)
103
145
  */
104
- type PascalToSnake<S extends string> = SeparatorCase<S, '_'>;
146
+ type PascalToCamelCase<S extends string> = S extends `${infer A}${infer B}${infer C}${infer Rest}` ? [A, B, C] extends [UpperLetter, UpperLetter, LowerLetter] ? C extends typeof ACRONYM_PLURAL_SUFFIX ? FirstChar<Rest> extends LowerLetter ? Uncapitalize<S> : LowercaseFirstWord<S> : Uncapitalize<S> : LowercaseFirstWord<S> : LowercaseFirstWord<S>;
147
+ /** snake_case/kebab-case to camelCase (`snakeToCamelCase`, `kebabToCamelCase`) */
148
+ type SeparatedToCamelCase<S extends string, Sep extends Separator, Acc extends string = ''> = S extends `${infer Word}${Sep}${infer Next}${infer Rest}` ? SeparatedToCamelCase<Rest, Sep, `${Acc}${Word}${Uppercase<Next>}`> : `${Acc}${S}`;
149
+ /** Replaces every `From` separator with `To` (`snakeToKebabCase`, `kebabToSnakeCase`) */
150
+ type ReplaceSeparator<S extends string, From extends Separator, To extends Separator, Acc extends string = ''> = S extends `${infer Head}${From}${infer Rest}` ? ReplaceSeparator<Rest, From, To, `${Acc}${Head}${To}`> : `${Acc}${S}`;
151
+ /** A single word in the target case (`transformSingleWord`) */
152
+ type SingleWordToCase<K extends string, To extends StringCase> = To extends 'pascal' ? Capitalize<K> : To extends 'camel' ? Uncapitalize<K> : Lowercase<K>;
153
+ /** A key of the detected case `From` in the target case (`formatterMap`) */
154
+ type FormatKey<K extends string, From extends StringCase, To extends StringCase> = From extends To ? K : From extends 'snake' | 'kebab' ? To extends 'camel' ? SeparatedToCamelCase<K, From extends 'snake' ? '_' : '-'> : To extends 'pascal' ? Capitalize<SeparatedToCamelCase<K, From extends 'snake' ? '_' : '-'>> : From extends 'snake' ? ReplaceSeparator<K, '_', '-'> : ReplaceSeparator<K, '-', '_'> : To extends 'pascal' ? Capitalize<K> : To extends 'camel' ? PascalToCamelCase<K> : ToSeparatedCase<K, To extends 'snake' ? '_' : '-'>;
155
+ /** Applies the detected case of `K` (`transformCase`) */
156
+ type TransformDetectedKey<K extends string, Detected, To extends StringCase> = Detected extends 'single' ? SingleWordToCase<K, To> : Detected extends StringCase ? FormatKey<K, Detected, To> : K;
105
157
  /**
106
- * Type utility that transforms a PascalCase string to kebab-case (acronym-aware)
107
- * Example: 'HelloWorld' -> 'hello-world', 'HTTPResponse' -> 'http-response'
158
+ * The key a string key `K` becomes when transformed to `To`. A `string` or
159
+ * template-literal key cannot be known exactly, so it becomes `string`.
108
160
  */
109
- type PascalToKebab<S extends string> = SeparatorCase<S, '-'>;
161
+ type TransformKey<K extends string, To extends StringCase> = IsStringLiteral<K> extends false ? string : K extends '' ? K : IsPreservedKey<K> extends true ? K : TransformDetectedKey<K, DetectedCase<K>, To>;
162
+
163
+ /** Primitive values, including branded ones such as `string & { __brand: 'UserId' }` */
164
+ type Primitive = string | number | boolean | bigint | symbol | null | undefined;
165
+ /**
166
+ * Values the runtime returns untouched that TypeScript can recognise: functions and
167
+ * classes, `Date`, `RegExp`, and built-ins that declare a `Symbol.toStringTag` (typed
168
+ * arrays, `ArrayBuffer`, `DataView`, `Promise`, `WeakMap`, `WeakSet`, ...).
169
+ *
170
+ * `Error`, `URL` and other host objects are returned untouched as well, but their types
171
+ * cannot be told apart from plain objects (an `Error` is any `{ name, message }`).
172
+ */
173
+ type UntouchedValue = Function | Date | RegExp | {
174
+ readonly [Symbol.toStringTag]: string;
175
+ };
110
176
  /**
111
- * Type utility that transforms a kebab-case string to camelCase
112
- * Example: 'hello-world' -> 'helloWorld'
177
+ * `true` once `Level` (the nesting level so far, as a tuple) reaches `Depth`.
178
+ * `Depth` is `undefined` when every level is transformed. The `undefined` check comes
179
+ * first and is not distributive, so it also holds without `strictNullChecks`; comparing
180
+ * `Depth extends Level['length']` also accepts a branded depth such as `1 & Brand`.
113
181
  */
114
- type KebabToCamel<S extends string> = S extends `${infer P}-${infer C}${infer R}` ? `${P}${Uppercase<C>}${KebabToCamel<R>}` : S;
182
+ type IsDepthReached<Depth extends number | undefined, Level extends unknown[]> = [Depth] extends [undefined] ? false : Depth extends Level['length'] ? true : false;
115
183
  /**
116
- * Type utility that transforms a kebab-case string to PascalCase
117
- * Example: 'hello-world' -> 'HelloWorld'
184
+ * The nesting level of the values of an object, Map or Set. With unlimited depth the
185
+ * level stays `[]`, so recursive data types reuse the same instantiation.
118
186
  */
119
- type KebabWordsToPascal<Parts extends string[]> = Parts extends [
120
- infer First extends string,
121
- ...infer Rest extends string[]
122
- ] ? `${Capitalize<First>}${KebabWordsToPascal<Rest>}` : '';
123
- type KebabToWords<S extends string> = S extends `${infer P}-${infer R}` ? [P, ...KebabToWords<R>] : [S];
187
+ type NextLevel<Depth extends number | undefined, Level extends unknown[]> = [
188
+ Depth
189
+ ] extends [undefined] ? Level : [...Level, unknown];
124
190
  /**
125
- * Type utility that transforms a kebab-case string to PascalCase
126
- * Example: 'hello-world' -> 'HelloWorld'
191
+ * Transforms `T` the way `internalTransform` does: arrays keep their nesting level,
192
+ * while the values of objects, Maps and Sets are one level deeper.
193
+ *
194
+ * Arrays are typed as arrays of transformed items, which lets TypeScript defer
195
+ * recursive types such as a JSON value type; only tuples are mapped position by position.
196
+ *
197
+ * Use `TransformResult` instead. This type is exported only so that declaration files
198
+ * can refer to it: the result of a generic wrapper around `transform` is written in terms
199
+ * of it, and an unexported recursive type would have to be expanded there without end.
127
200
  */
128
- type KebabToPascal<S extends string> = S extends `${infer P}-${infer R}` ? KebabWordsToPascal<[P, ...KebabToWords<R>]> : Capitalize<S>;
201
+ type TransformValue<T, To extends StringCase, Depth extends number | undefined, Level extends unknown[]> = IsAny<T> extends true ? T : IsDepthReached<Depth, Level> extends true ? T : T extends Primitive ? T : T extends readonly (infer Item)[] ? number extends T['length'] ? T extends unknown[] ? TransformValue<Item, To, Depth, Level>[] : readonly TransformValue<Item, To, Depth, Level>[] : {
202
+ [I in keyof T]: TransformValue<T[I], To, Depth, Level>;
203
+ } : T extends ReadonlyMap<infer K, infer V> ? Map<K extends string ? TransformKey<K, To> : K, TransformValue<V, To, Depth, NextLevel<Depth, Level>>> : T extends ReadonlySet<infer V> ? Set<TransformValue<V, To, Depth, NextLevel<Depth, Level>>> : T extends UntouchedValue ? T : T extends object ? {
204
+ [K in keyof T as K extends string ? TransformKey<K, To> : K]: K extends symbol ? T[K] : TransformValue<T[K], To, Depth, NextLevel<Depth, Level>>;
205
+ } : T;
129
206
  /**
130
- * Type utility that transforms a kebab-case string to snake_case
131
- * Example: 'hello-world' -> 'hello_world'
207
+ * Applies a numeric literal `Depth`: a negative depth transforms nothing, and a
208
+ * fractional depth is not supported precisely, so its result is `unknown`.
132
209
  */
133
- type KebabToSnake<S extends string> = S extends `${infer P}-${infer R}` ? `${P}_${KebabToSnake<R>}` : S;
210
+ type TransformWithDepth<T, To extends StringCase, Depth extends number> = `${Depth}` extends `-${string}` ? T : `${Depth}` extends `${string}${'.' | 'e'}${string}` ? unknown : TransformValue<T, To, Depth, []>;
134
211
  /**
135
- * Utility type to detect preserved keys that shouldn't be transformed.
136
- * Preserved keys include those with leading/trailing special characters,
137
- * unsupported patterns, or invalid starting/ending characters.
212
+ * `true` for a numeric literal, branded or not (`2`, `2 & Brand`), and `false` for
213
+ * `number` and a branded `number`: a literal shares no value with at least one of two
214
+ * different literals, while `number` shares one with both.
138
215
  */
139
- type ForbiddenStartChars = '_' | '-' | '$' | '&' | '%' | '#' | '@' | '!' | '*' | '+' | '=' | '~' | '`' | '^' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '\\' | ':' | ';' | '"' | "'" | '<' | '>' | ',' | '.' | '?' | '/' | `${number}`;
140
- type ForbiddenEndChars = '_' | '-' | '$' | '&' | '%' | '#' | '@' | '!' | '*' | '+' | '=' | '~' | '`' | '^' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '\\' | ':' | ';' | '"' | "'" | '<' | '>' | ',' | '.' | '?' | '/' | `${number}`;
216
+ type IsNumberLiteral<N extends number> = [N & 0] extends [never] ? true : [N & 1] extends [never] ? true : false;
141
217
  /**
142
- * The character set the runtime is willing to transform. Any key containing a
143
- * character outside it — a symbol, or a non-ASCII letter like `é` — is preserved
144
- * wholesale at runtime (`/[^A-Za-z0-9_-]/`), so the type must preserve it too.
145
- * An explicit ASCII set is required because TS's `Uppercase`/`Lowercase` are
146
- * Unicode-aware and would otherwise treat `é` as an ordinary transformable letter.
218
+ * The result for one `Depth` (distributes over a union of depths): every level for
219
+ * `undefined`, `unknown` for a depth that is only known to be a `number`.
147
220
  */
148
- type AllowedKeyChar = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '_' | '-';
221
+ type TransformToDepth<T, To extends StringCase, Depth extends number | undefined> = Depth extends unknown ? [Depth] extends [undefined] ? TransformValue<T, To, undefined, []> : IsNumberLiteral<Depth & number> extends true ? TransformWithDepth<T, To, Depth & number> : unknown : never;
149
222
  /**
150
- * True if `S` contains any character outside `AllowedKeyChar`. Mirrors the runtime's
151
- * `/[^A-Za-z0-9_-]/` preserve check.
223
+ * The exact type `transform` returns for `data` of type `T`, transformed to `To` (default
224
+ * `'camel'`) for `Depth` nesting levels (default: all of them).
225
+ *
226
+ * `To` and `Depth` distribute over unions: when they are only known to be one of several
227
+ * values, the result is the union of the possible results. A depth that is not a numeric
228
+ * literal (`number`, including `Infinity`) cannot be known, so the result is `unknown`,
229
+ * as it is for a `To` or `Depth` typed `any`. String enum members and branded literals
230
+ * are accepted for `To`.
231
+ *
232
+ * @example
233
+ * ```typescript
234
+ * type User = TransformResult<{ user_id: number; created_at: Date }>;
235
+ * // => { userId: number; createdAt: Date }
236
+ * ```
152
237
  */
153
- type HasOutOfCharsetChar<S extends string> = S extends `${infer C}${infer R}` ? C extends AllowedKeyChar ? HasOutOfCharsetChar<R> : true : false;
154
- type PreservedKey<K extends string> = HasOutOfCharsetChar<K> extends true ? true : K extends `${infer First}${string}` ? First extends ForbiddenStartChars ? true : K extends `${string}${infer Last}` ? Last extends ForbiddenEndChars ? true : K extends `${string}__${string}` | `${string}--${string}` ? true : false : false : false;
238
+ type TransformResult<T, To extends StringCase = 'camel', Depth extends number | undefined = undefined> = IsAny<To> extends true ? unknown : IsAny<Depth> extends true ? unknown : To extends unknown ? TransformToDepth<T, PlainCase<To>, Depth> : never;
239
+ /** `true` when `Options` may omit `Key` (an optional property) */
240
+ type IsOptional<Options, Key extends PropertyKey> = Key extends keyof Options ? Record<never, never> extends Pick<Options, Key> ? true : false : false;
241
+ /** `true` when the option `Key` of `Options` is typed `any` */
242
+ type IsAnyOption<Options, Key extends PropertyKey> = Key extends keyof Options ? IsAny<Options[Key]> : false;
155
243
  /**
156
- * Type utility that selects the correct string transformation based on source and target casing
244
+ * Includes `true` when a member of a union of options objects has a `targetCase` typed
245
+ * `any` (check it with `true extends HasAnyTargetCase<Options>`)
157
246
  */
158
- type TransformKey<K extends string, O extends TransformOptions> = PreservedKey<K> extends true ? K : undefined extends O['sourceCase'] ? string : O['sourceCase'] extends 'snake' ? undefined extends O['targetCase'] ? SnakeToCamel<K> : O['targetCase'] extends 'camel' ? SnakeToCamel<K> : O['targetCase'] extends 'pascal' ? SnakeToPascal<K> : O['targetCase'] extends 'kebab' ? SnakeToKebab<K> : K : O['sourceCase'] extends 'camel' ? undefined extends O['targetCase'] ? K : O['targetCase'] extends 'snake' ? CamelToSnake<K> : O['targetCase'] extends 'pascal' ? CamelToPascal<K> : O['targetCase'] extends 'kebab' ? CamelToKebab<K> : K : O['sourceCase'] extends 'pascal' ? undefined extends O['targetCase'] ? PascalToCamel<K> : O['targetCase'] extends 'camel' ? PascalToCamel<K> : O['targetCase'] extends 'snake' ? PascalToSnake<K> : O['targetCase'] extends 'kebab' ? PascalToKebab<K> : K : O['sourceCase'] extends 'kebab' ? undefined extends O['targetCase'] ? KebabToCamel<K> : O['targetCase'] extends 'camel' ? KebabToCamel<K> : O['targetCase'] extends 'pascal' ? KebabToPascal<K> : O['targetCase'] extends 'snake' ? KebabToSnake<K> : K : K;
247
+ type HasAnyTargetCase<Options> = Options extends unknown ? IsAnyOption<Options, 'targetCase'> : never;
159
248
  /**
160
- * Recursive type transformations
161
- * These types handle nested structures like arrays, objects, Maps, and Sets
249
+ * The result for one options object. An optional `targetCase` or `depth` may also be
250
+ * omitted, which uses its default; those results are added explicitly because, without
251
+ * `strictNullChecks`, `undefined` disappears from the property's type.
162
252
  */
163
- type SnakeToCamelRecursive<T> = T extends Array<infer U> ? Array<SnakeToCamelRecursive<U>> : T extends Map<infer K, infer V> ? Map<K extends string ? SnakeToCamel<K> : K, SnakeToCamelRecursive<V>> : T extends Set<infer V> ? Set<SnakeToCamelRecursive<V>> : T extends object ? {
164
- [K in keyof T as TransformKey<K & string, {
165
- sourceCase: 'snake';
166
- targetCase: 'camel';
167
- }>]: SnakeToCamelRecursive<T[K]>;
168
- } : T;
169
- type SnakeToPascalRecursive<T> = T extends Array<infer U> ? Array<SnakeToPascalRecursive<U>> : T extends Map<infer K, infer V> ? Map<K extends string ? SnakeToPascal<K> : K, SnakeToPascalRecursive<V>> : T extends Set<infer V> ? Set<SnakeToPascalRecursive<V>> : T extends object ? {
170
- [K in keyof T as TransformKey<K & string, {
171
- sourceCase: 'snake';
172
- targetCase: 'pascal';
173
- }>]: SnakeToPascalRecursive<T[K]>;
174
- } : T;
175
- type SnakeToKebabRecursive<T> = T extends Array<infer U> ? Array<SnakeToKebabRecursive<U>> : T extends Map<infer K, infer V> ? Map<K extends string ? SnakeToKebab<K> : K, SnakeToKebabRecursive<V>> : T extends Set<infer V> ? Set<SnakeToKebabRecursive<V>> : T extends object ? {
176
- [K in keyof T as TransformKey<K & string, {
177
- sourceCase: 'snake';
178
- targetCase: 'kebab';
179
- }>]: SnakeToKebabRecursive<T[K]>;
180
- } : T;
181
- type CamelToSnakeRecursive<T> = T extends Array<infer U> ? Array<CamelToSnakeRecursive<U>> : T extends Map<infer K, infer V> ? Map<K extends string ? CamelToSnake<K> : K, CamelToSnakeRecursive<V>> : T extends Set<infer V> ? Set<CamelToSnakeRecursive<V>> : T extends object ? {
182
- [K in keyof T as TransformKey<K & string, {
183
- sourceCase: 'camel';
184
- targetCase: 'snake';
185
- }>]: CamelToSnakeRecursive<T[K]>;
186
- } : T;
187
- type CamelToPascalRecursive<T> = T extends Array<infer U> ? Array<CamelToPascalRecursive<U>> : T extends Map<infer K, infer V> ? Map<K extends string ? CamelToPascal<K> : K, CamelToPascalRecursive<V>> : T extends Set<infer V> ? Set<CamelToPascalRecursive<V>> : T extends object ? {
188
- [K in keyof T as TransformKey<K & string, {
189
- sourceCase: 'camel';
190
- targetCase: 'pascal';
191
- }>]: CamelToPascalRecursive<T[K]>;
192
- } : T;
193
- type CamelToKebabRecursive<T> = T extends Array<infer U> ? Array<CamelToKebabRecursive<U>> : T extends Map<infer K, infer V> ? Map<K extends string ? CamelToKebab<K> : K, CamelToKebabRecursive<V>> : T extends Set<infer V> ? Set<CamelToKebabRecursive<V>> : T extends object ? {
194
- [K in keyof T as TransformKey<K & string, {
195
- sourceCase: 'camel';
196
- targetCase: 'kebab';
197
- }>]: CamelToKebabRecursive<T[K]>;
198
- } : T;
199
- type PascalToCamelRecursive<T> = T extends Array<infer U> ? Array<PascalToCamelRecursive<U>> : T extends Map<infer K, infer V> ? Map<K extends string ? PascalToCamel<K> : K, PascalToCamelRecursive<V>> : T extends Set<infer V> ? Set<PascalToCamelRecursive<V>> : T extends object ? {
200
- [K in keyof T as TransformKey<K & string, {
201
- sourceCase: 'pascal';
202
- targetCase: 'camel';
203
- }>]: PascalToCamelRecursive<T[K]>;
204
- } : T;
205
- type PascalToSnakeRecursive<T> = T extends Array<infer U> ? Array<PascalToSnakeRecursive<U>> : T extends Map<infer K, infer V> ? Map<K extends string ? PascalToSnake<K> : K, PascalToSnakeRecursive<V>> : T extends Set<infer V> ? Set<PascalToSnakeRecursive<V>> : T extends object ? {
206
- [K in keyof T as TransformKey<K & string, {
207
- sourceCase: 'pascal';
208
- targetCase: 'snake';
209
- }>]: PascalToSnakeRecursive<T[K]>;
210
- } : T;
211
- type PascalToKebabRecursive<T> = T extends Array<infer U> ? Array<PascalToKebabRecursive<U>> : T extends Map<infer K, infer V> ? Map<K extends string ? PascalToKebab<K> : K, PascalToKebabRecursive<V>> : T extends Set<infer V> ? Set<PascalToKebabRecursive<V>> : T extends object ? {
212
- [K in keyof T as TransformKey<K & string, {
213
- sourceCase: 'pascal';
214
- targetCase: 'kebab';
215
- }>]: PascalToKebabRecursive<T[K]>;
216
- } : T;
217
- type KebabToCamelRecursive<T> = T extends Array<infer U> ? Array<KebabToCamelRecursive<U>> : T extends Map<infer K, infer V> ? Map<K extends string ? KebabToCamel<K> : K, KebabToCamelRecursive<V>> : T extends Set<infer V> ? Set<KebabToCamelRecursive<V>> : T extends object ? {
218
- [K in keyof T as TransformKey<K & string, {
219
- sourceCase: 'kebab';
220
- targetCase: 'camel';
221
- }>]: KebabToCamelRecursive<T[K]>;
222
- } : T;
223
- type KebabToPascalRecursive<T> = T extends Array<infer U> ? Array<KebabToPascalRecursive<U>> : T extends Map<infer K, infer V> ? Map<K extends string ? KebabToPascal<K> : K, KebabToPascalRecursive<V>> : T extends Set<infer V> ? Set<KebabToPascalRecursive<V>> : T extends object ? {
224
- [K in keyof T as TransformKey<K & string, {
225
- sourceCase: 'kebab';
226
- targetCase: 'pascal';
227
- }>]: KebabToPascalRecursive<T[K]>;
228
- } : T;
229
- type KebabToSnakeRecursive<T> = T extends Array<infer U> ? Array<KebabToSnakeRecursive<U>> : T extends Map<infer K, infer V> ? Map<K extends string ? KebabToSnake<K> : K, KebabToSnakeRecursive<V>> : T extends Set<infer V> ? Set<KebabToSnakeRecursive<V>> : T extends object ? {
230
- [K in keyof T as TransformKey<K & string, {
231
- sourceCase: 'kebab';
232
- targetCase: 'snake';
233
- }>]: KebabToSnakeRecursive<T[K]>;
234
- } : T;
253
+ type TransformResultForOne<T, Options, To extends StringCase = TargetCaseOf<Options>, Depth extends number | undefined = DepthOf<Options> extends infer D extends number | undefined ? D : never> = TransformResult<T, To, Depth> | (IsOptional<Options, 'targetCase'> extends true ? TransformResult<T, 'camel', Depth> : never) | (IsOptional<Options, 'depth'> extends true ? TransformResult<T, To, undefined> : never) | (IsOptional<Options, 'targetCase'> extends true ? IsOptional<Options, 'depth'> extends true ? TransformResult<T, 'camel', undefined> : never : never);
235
254
  /**
236
- * Generic recursive transformation type
237
- * Allows dynamically selecting the transformation based on source and target cases
255
+ * The result type for an options object. Each member of a union of options objects gives
256
+ * its own result, so its `targetCase` and `depth` stay paired. Options typed `any`, or
257
+ * with a `targetCase` or `depth` typed `any`, say nothing about the result: `unknown`.
238
258
  */
239
- type TransformRecursive<T, FromCase extends StringCase, ToCase extends StringCase> = FromCase extends 'snake' ? ToCase extends 'camel' ? SnakeToCamelRecursive<T> : ToCase extends 'pascal' ? SnakeToPascalRecursive<T> : ToCase extends 'kebab' ? SnakeToKebabRecursive<T> : T : FromCase extends 'camel' ? ToCase extends 'snake' ? CamelToSnakeRecursive<T> : ToCase extends 'pascal' ? CamelToPascalRecursive<T> : ToCase extends 'kebab' ? CamelToKebabRecursive<T> : T : FromCase extends 'pascal' ? ToCase extends 'snake' ? PascalToSnakeRecursive<T> : ToCase extends 'camel' ? PascalToCamelRecursive<T> : ToCase extends 'kebab' ? PascalToKebabRecursive<T> : T : FromCase extends 'kebab' ? ToCase extends 'snake' ? KebabToSnakeRecursive<T> : ToCase extends 'camel' ? KebabToCamelRecursive<T> : ToCase extends 'pascal' ? KebabToPascalRecursive<T> : T : T;
259
+ type TransformResultFor<T, Options> = IsAny<Options> extends true ? unknown : Options extends unknown ? IsAnyOption<Options, 'targetCase'> extends true ? unknown : IsAnyOption<Options, 'depth'> extends true ? unknown : TransformResultForOne<T, Options> : never;
240
260
 
241
- declare function transform<T, From extends StringCase, To extends StringCase>(data: T, options: TransformOptions & {
242
- depth?: number;
261
+ /**
262
+ * Transforms the keys of `data` to camelCase, at every nesting level.
263
+ *
264
+ * Every key of the result type is typed the way the runtime transforms it, except in
265
+ * values TypeScript cannot tell apart from plain objects (see "Result types" in the
266
+ * readme).
267
+ */
268
+ declare function transform<T>(data: T, options?: undefined): TransformResult<T>;
269
+ /**
270
+ * Transforms the keys of `data` to `targetCase` (default `'camel'`), at every nesting
271
+ * level unless `depth` is set.
272
+ *
273
+ * Every key of the result type is typed the way the runtime transforms it, except in
274
+ * values TypeScript cannot tell apart from plain objects (see "Result types" in the
275
+ * readme).
276
+ *
277
+ * `To` and `Depth` capture the literal values of `targetCase` and `depth`. `Options`
278
+ * captures the options object as a whole, so a value that may be `undefined` (which the
279
+ * runtime treats as omitted) is reflected in the result type, and each member of a union
280
+ * of options objects gets its own result. `NoUnknownOptions` keeps rejecting unknown
281
+ * option names. An options object that may itself be `undefined` matches neither this
282
+ * overload nor the one above, and gets an `unknown` result type from the last one.
283
+ *
284
+ * The type parameter defaults only apply when type arguments are passed explicitly, and
285
+ * give an `unknown` result type. An explicit `any` as the second type argument was the
286
+ * output type before `To` existed, so it still gives `any` (`To` is `any` there, while
287
+ * the options keep their default type); a `targetCase` typed `any` gives `unknown`.
288
+ */
289
+ declare function transform<T, To extends StringCase | undefined = StringCase | undefined, Depth extends number | undefined = number | undefined, Options extends TransformOptions = TransformOptions>(data: T, options: Options & {
290
+ targetCase?: To;
291
+ depth?: Depth;
292
+ } & NoUnknownOptions<Options>): IsAny<To> extends true ? true extends HasAnyTargetCase<Options> ? unknown : To : TransformResultFor<T, Options>;
293
+ /**
294
+ * The 2.x form with explicit `<Data, SourceCase, TargetCase>` type arguments.
295
+ *
296
+ * @deprecated `sourceCase` has no effect: leave out the type arguments and `sourceCase`,
297
+ * and the result type is inferred, `depth` included. This form does not accept `depth`.
298
+ */
299
+ declare function transform<T, From extends StringCase, To extends StringCase>(data: T, options: {
243
300
  sourceCase: From;
244
301
  targetCase: To;
245
- }): TransformRecursive<T, From, To>;
302
+ depth?: undefined;
303
+ }): TransformResult<T, To>;
246
304
  /**
247
305
  * Transform data to a specified output type.
248
306
  *
249
307
  * Note: When using this overload, type safety is not enforced between the input, options, and output type.
250
- * The 'sourceCase' option is intentionally omitted, as it is not used for type inference here.
251
- */
252
- declare function transform<T, K>(data: T, options?: Omit<TransformOptions, 'sourceCase'> & {
253
- depth?: number;
254
- }): K;
255
- declare function transform<T>(data: T, options?: TransformOptions & {
256
- depth?: number;
257
- }): unknown;
308
+ * The deprecated 'sourceCase' option is not accepted here, as it has no effect.
309
+ */
310
+ declare function transform<T, K>(data: T, options?: Omit<TransformOptions, 'sourceCase'>): K;
258
311
 
259
312
  export { transform };
260
- export type { StringCase, TransformOptions };
313
+ export type { StringCase, TransformOptions, TransformResult, TransformResultFor, TransformValue };