sindresorhus / type-fest

A collection of essential TypeScript types
Creative Commons Zero v1.0 Universal
14.12k stars 533 forks source link

snakeCasedProperties and camelCasedProperties have different behaviours when parsing numbers #336

Open olivierbeaulieu opened 2 years ago

olivierbeaulieu commented 2 years ago

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

Fund with Polar

kmordan24 commented 2 years ago

@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.

voxpelli commented 1 year ago

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 commented 1 year ago

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 commented 1 month ago

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.

@fregante Are we sure its a bug? Would still want some references here that indicates that camel cased fooBar1 should become foo_bar_1 rather than foo_bar1

fregante commented 1 month ago

Feel free to change the label. It looked like a bug report when I fast-triaged 80 issues in the repo 😅