Búsqueda de sitios web

Typescript hace coincidir la primera letra en mayúscula a nivel de tipo


Quiero transformar un literal de cadena de camelCase a snake_case, como:

type CamelCaseStr = "helloWorldAgain"
type _ = ToSnakeCase<CamelCaseStr> // "hello_world_again"

Aquí está mi solución:

type str = "helloWorldAgain";
type UpperCollection = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
type Chars<T extends string> = T extends `${infer Char}${infer Rest}` ? Char | Chars<Rest> : never;
type UpperLetters = Chars<UpperCollection>;

type ToSnakeCase<T extends string> = 
T extends `${infer LowerStr}${UpperLetters}${infer AfterFirstBigLetter}` ? 
T extends `${LowerStr}${infer WithFirstBigLetter}` ? 
`${LowerStr}_${ToSnakeCase<Uncapitalize<WithFirstBigLetter>>}` : 
LowerStr : 
T;
type _ = ToSnakeCase<str>; 
// type _ = "helloWorld_again" | "helloWorld_world_again" | "hello_again" | "hello_world_again"

Intenté extraer un LowerStr hasta encontrar primero una letra en mayúscula, por lo que extraigo la primera cadena capitalizada, luego la Uncapitalize y uso ToSnakeCase nuevamente. Sin embargo, el resultado es incorrecto, así que escribí algo de código de prueba.

type TestToSnakeCase<T extends string> = 
T extends `${infer LowerStr}${UpperLetters}${infer AfterFirstBigLetter}` ? 
T extends `${LowerStr}${infer WithFirstBigLetter}` ? 
[LowerStr, WithFirstBigLetter] : 
never
: T;
type __ = TestToSnakeCase<str>; 
// type __ = ["helloWorld" | "hello", "Again" | "WorldAgain"];

El problema parece reducirse a la discriminación de cierre. Busqué y encontré este enlace útil, con el cual intenté nuevamente.

type ShutDiscrimination<T extends string> = 
[T] extends [`${infer LowerStr}${UpperLetters}${infer AfterFirstBigLetter}`] ? 
[T] extends [`${LowerStr}${infer WithFirstBigLetter}`] ? 
`${LowerStr}_${ShutDiscrimination<Uncapitalize<WithFirstBigLetter>>}` : 
LowerStr : 
T;
type ___ = ShutDiscrimination<str>;
// type ___ = "helloWorld_again" | "helloWorld_worldAgain" | "hello_again" | "hello_worldAgain"

¿Hay algo crítico que me haya perdido para resolver este problema? Espero haber expresado bien mi idea, el formato es bastante engorroso:)

enlace del parque

Respuesta #1

El problema principal con tu código es que estás usando UpperLetters directamente en la cláusula condicional donde haces la inferencia. Debido a que UpperLetters solo puede ser un carácter, en su lugar deberías inferir el carácter y luego verificar si extends UpperLetters. Esto aplanará las uniones que ves.

type UpperLetter = "a" | "b";
type Parse<S extends string> = S extends
  `${infer $Start}${infer $UpperLetter}${infer $Rest}`
  ? $UpperLetter extends UpperLetter ? true : false
  : false;

De todos modos, intenté el problema yo mismo sin usar una unión UpperLetter estática comprobando si Capitalize<S> es igual a S. Aquí está:

type CamelToSnakeCase<S extends string, $Acc extends string = ""> =
  //
  S extends `${infer $Ch}${infer $Rest}`
    ? S extends Capitalize<S>
      ? CamelToSnakeCase<$Rest, `${$Acc}_${Lowercase<$Ch>}`>
    : CamelToSnakeCase<$Rest, `${$Acc}${$Ch}`>
    : $Acc;

type foo = CamelToSnakeCase<"helloAwesomeWorld">; // "hello_awesome_world"
type foo2 = CamelToSnakeCase<"">; // ""
type foo3 = CamelToSnakeCase<"bruhSOCool">; // "bruh_s_o_cool"

type SnakeToCamelCase<S extends string, $Acc extends string = ""> =
  //
  S extends `${infer $Ch}${infer $Rest}`
    ? $Ch extends "_"
      ? $Rest extends `${infer $Ch}${infer $Rest}`
        ? SnakeToCamelCase<$Rest, `${$Acc}${Capitalize<$Ch>}`>
      : ""
    : SnakeToCamelCase<$Rest, `${$Acc}${$Ch}`>
    : $Acc;

type asdf = SnakeToCamelCase<"hello_awesome_world">; // "helloAwesomeWorld"
type asdf2 = SnakeToCamelCase<"bruh_s_o_cool">; // "bruhSOCool"
type asdf3 = SnakeToCamelCase<"">; // ""
type asdf4 = SnakeToCamelCase<"so_cool_bro">; // "soCoolBro"

Enlace al Playground de TypeScript

Edit: No fue la unión la que arregló tu código. Fue el hecho de que inferí la siguiente letra cuando hice $ {infer $Ch}$ {infer $Rest} y luego verifiqué si era mayúscula con Capitalize. Tú estabas haciendo $ {infer LowerStr}$ {UpperLetters}$ {infer AfterFirstBigLetter} lo cual calcula el valor de la cadena para cada rama en UpperLetters y eso provoca problemas con múltiples tipos para LowerStr y AfterFirstBigLetter. Aquí hay un ejemplo:

type UpperLetter = "A" | "B" | "C" | "D" // ..

// This is going to go through each computed type in the extends clause and come up with multiple 
// answers in the union for after the various characters.

type bad = "ABCD" extends `${string}${UpperLetter}${infer $After}` ? $After : never; // "" | "D" | "BCD" | "CD"

// Now, let's fix the code by writing an infer character to force the extends clause to only have one possible combination.
// This can be done by replacing `UpperLetter` with `infer $Ch` because any `string` / `infer $Ch` and removing the leading
// `${string}` part.

type fixed = "ABCD" extends `${infer $Ch}${infer $After}` ? $Ch extends UpperLetter ? $After : never : never; // "BCD"

Enlace al Playground de TypeScript

Respuesta #2

Aquí está mi solución, inspirada por @sno2

Me he centrado en que solo hay una manera de comparar el 'personaje objetivo' con recursiones sin Colección de Alfabeto.

export type CamelCaseToSnakeCase<
  T extends string,
  Joiner extends '' | '_' = ''
> = T extends `${infer Character}${infer Rest}`
  ? Character extends Uppercase<Character>
    ? `${Joiner}${Lowercase<Character>}${CamelCaseToSnakeCase<Rest, '_'>}`
    : `${Character}${CamelCaseToSnakeCase<Rest, '_'>}`
  : '';

Artículos relacionados