type-fest icon indicating copy to clipboard operation
type-fest copied to clipboard

snakeCasedProperties and camelCasedProperties have different behaviours when parsing numbers

Open olivierbeaulieu opened this issue 3 years ago • 5 comments

Given the following example:

type F = SnakeCasedPropertiesDeep<
  CamelCasedPropertiesDeep<{
    foo_1: boolean;
  }>
>;

I would expect each operation to be the opposite operation of each other - resulting in F being back to its original value F = { foo_1: boolean }.

That is not the current behaviour, we as have F = { foo1: boolean }.

Is that behaviour correct? If so, how can I get back to my original value when jumping from one case to the other?

For context, I'm trying to match the result of lodash's snakeCase

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • The funding will be given to active contributors.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

olivierbeaulieu avatar Dec 09 '21 16:12 olivierbeaulieu

@olivierbeaulieu I ran into this same issue.

I absolutely love this library, but I really do feel like char->number transitions should add underscore as well. It might be reasonable to not make this assumption, especially with something like { "http2": true } where we would NOT want http2 -> http_2. There is no clear standard on this but the most common approach is "column_1" rather than "column1" if you are doing everything in snake case...

Here are some Frankenstein'd types.... That being said this is not nearly as elegant as the type definitions written in this library, so idk.

Snake To Camel Case:

export type CamelizeInputType = Record<PropertyKey, any> | Array<any>;

export type SnakeToCamelCase<S extends PropertyKey> = S extends number
  ? S
  : S extends `${infer T}_${infer U}`
  ? `${T}${Capitalize<SnakeToCamelCase<U>>}`
  : S;

export type SnakeToCamelCaseNested<T> = T extends Function | RegExp | Date
  ? T
  : T extends (infer E)[]
  ? SnakeToCamelCaseNested<E>[]
  : T extends CamelizeInputType
  ? {
      [K in keyof T as SnakeToCamelCase<Extract<K, PropertyKey>>]: SnakeToCamelCaseNested<T[K]>;
    }
  : T;

Camel To Snake Case

export type SnakeCaseInputType = Record<PropertyKey, any> | Array<any>;
type UpperAlphabetic =
  | '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';

type AlphanumericDigits = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '0';

/**
 * Return underscore if it is allowed between provided characters,
 * trail and lead underscore are allowed, empty string is considered
 * as the beginning of a string.
 */
type SnakeUnderscore<
  First extends PropertyKey,
  Second extends PropertyKey
> = First extends AlphanumericDigits
  ? Second extends UpperAlphabetic
    ? '_'
    : ''
  : First extends UpperAlphabetic | '' | '_'
  ? ''
  : Second extends UpperAlphabetic | AlphanumericDigits
  ? '_'
  : '';

/**
 * Convert string literal type to snake_case
 */
type CamelToSnakeCase<S extends PropertyKey, Previous extends PropertyKey = ''> = S extends number
  ? S
  : S extends `${infer First}${infer Second}${infer Rest}`
  ? `${SnakeUnderscore<Previous, First>}${Lowercase<First>}${SnakeUnderscore<
      First,
      Second
    >}${Lowercase<Second>}${CamelToSnakeCase<Rest, First>}`
  : S extends `${infer First}`
  ? `${SnakeUnderscore<Previous, First>}${Lowercase<First>}`
  : '';

edit: lodash won't 100% match these types. they are close but not all there.

kmordan24 avatar Jun 02 '22 21:06 kmordan24

Would love some more references on how this is dealt with in other projects.

That a snake cased foo_bar_1 should become fooBar1 camel cased is obvious.

That a camel cased fooBar1 should become foo_bar_1 or foo_bar1 is not as obvious to me.

foo_bar1 and foo_bar_1 both gets converted to fooBar1 when camel cased and hence only one of them can be converted the reverse way without an issue. Fixing foo_bar_1 will break foo_bar1 and as thus is potentially quite the breaking change.

voxpelli avatar Oct 13 '22 13:10 voxpelli

Kind of relates to #224 and #488 in that both of those also refer to lodash and how lodash does things in regards to this

voxpelli avatar Oct 13 '22 13:10 voxpelli