2

我有一个类型实用程序DashUppercase将字符串中的任何大写字符转换为-后跟小写等效字符。它的类型如下:

type LowerAlpha = "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 UpperAlpha = Uppercase<LowerAlpha>;

type Replace<S extends string, W extends string, P extends string> =
  S extends '' ? '' : W extends '' ? S :
  S extends `${infer F}${W}${infer E}` ? `${F}${P}${E}` : S

type DashUppercase<S extends string> = S extends `${infer START}${UpperAlpha}${infer REST}`
  ? DashUppercase<`${START}-${Uncapitalize<Replace<S, START, "">>}`>
  : S;

Replace实用程序 simple 允许我提取 START 字符串,以便我可以取消剩余的大写。

当第一个字符不是大写字母但传入的字符串文字以大写字母开头时,它会完全按照预期的方式旋转:

工作和不工作的图像

我确实尝试通过创建以下长度的条件来解决这个问题START

export type StringLength<S extends string, A extends any[] = []> = S extends '' 
  ? A['length'] 
  : S extends `${infer First}${infer Rest}` ? StringLength<Rest, [First, ...A]> : never

export type DashUppercase<S extends string> = S extends `${infer START}${UpperAlpha}${infer REST}`
  ? StringLength<START> extends 0
    ? DashUppercase<`${Uncapitalize<S>}`>
    : DashUppercase<`${START}-${Uncapitalize<Replace<S, START, "">>}`>
  : S;

认为这会给我一个简单的机会,S在开始之前将大写字母转换为非大写字母版本。可悲的是,结果是一样的。谁能帮我找出我做错了什么?

操场

4

2 回答 2

2

使用模板文字类型解析字符串的方法大致有两种,它们都有一些注意事项:


逐个字符

到目前为止,最简单的方法是逐个字符地遍历字符串,如下所示:

type Something<T extends string> = 
   T extends `${infer C0}${infer R}` ? Combine<C0, Something<R>> : 
   BaseCase

whereC0是字符串的第一个字符,R其余的是。当您有两个infer相邻的占位符时,编译器将推断第一个占位符的单个字符。这很简单,因为您总是知道会发生什么C0。主要的警告是递归限制足够浅,这样的东西只会对长度不超过 20 的字符串起作用。您可以稍微修改它,使接受的最大字符串长度大致增加一倍或三倍,方法是在可能的情况下一次抓取两个或三个字符。例如:

type Something<T extends string> =
  T extends `${infer C0}${infer C1}${infer R}` ? Combine<C0, Combine<C1, Something<R>>> :
  T extends `${infer C0}${infer R}` ? Combine<C0, Something<R>> : 
  BaseCase

如果我们DashUppercase使用这种方法,我们会得到:

type _DU<T extends string> = T extends Lowercase<T> ? T : `-${Lowercase<T>}`;

type DashUppercase<T extends string> =
  T extends `${infer C0}${infer R}` ? `${_DU<C0>}${DashUppercase<R>}` :
  ""

这很简单。请注意,我没有保留大写字符列表,而是将大写字符标识T为当您应用Lowercase<T>它时会发生变化的东西。它适用于您的用例(我认为):

type A = DashUppercase<'oneTwoThree'> // "one-two-three"
type B = DashUppercase<'OneTwoThree'> // "-one-two-three"
type C = DashUppercase<'23Skidoo'> // "23-skidoo"

但是,正如我所说,长字符串会导致递归警告:

type Oops = 
  DashUppercase<'abcdefghijklmnopqrstuvwxyz'> // error, too long, excessively deep

如果我将其更改为二乘二,您将得到:

type DashUppercase<T extends string> =
  T extends `${infer C0}${infer C1}${infer R}` ? 
    `${_DU<C0>}${_DU<C1>}${DashUppercase<R>}` :
  T extends `${infer C0}${infer R}` ? `${_DU<C0>}${DashUppercase<R>}` :
  ""

type Okay = DashUppercase<'abcdefghijklmnopqrstuvwxyz'> // okay now

type Oops = 
  DashUppercase<'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvw'> // too long

对于您的用例而言,这可能足够长,也可能不够长。


分隔符分割

您正在尝试做的另一种方法是在分隔符处拆分字符串。这可能有一个看起来像其中的部分T extends `${infer F}${D}${infer R}`,其中F是字符串的第一部分,D是一些定界符或定界符的联合,R是字符串的其余部分。这样做的吸引力在于,假设您的输入字符串中的分隔符很少,您将不会遇到递归限制。主要缺点是它很复杂,很难知道会出现什么FR

如果有多个候选者,编译器将倾向于在这里推断联合;没有什么能阻止F包含D. 这就是在你的定义中咬你的东西。因此,您将希望从F包含 aD本身的任何内容中消除,然后如果您需要识别分隔符(如果D是联合并且您关心匹配的成员),则需要再次推断字符串的其余部分。

在这个 GitHub 评论中,我编写了一个通用Break<T, D>实用程序,它接受一个字符串T并将其拆分为形式的元组,[F, R]其中F最长的前缀不包含D,并且R是字符串的其余部分。它看起来像这样:

type Break<T extends string, D extends string> = (
  string extends T ? [string, string] : (
    T extends `${infer F}${D}${infer R}` ? (
      F extends `${infer X}${D}${infer Y}` ? never : (
        T extends `${F}${infer R}` ? [F, R] : never
      )
    ) : [T, ""]
  )
);

呸!但它似乎确实有效。


有了这个,我们可以DashUppercase这样写:

type DashUppercase<T extends string> =
  Break<T, UpperAlpha> extends [infer L, infer R] ?
  L extends string ? R extends `${infer U}${infer RR}` ?
  `${L}-${Lowercase<U>}${DashUppercase<RR>}` : L :
  never : never;

所以我们Break T通过UpperAlpha分隔符集(我们确实需要它;在我看来,反对这种方法的另一点)。返回L的不包含大写字符,所以我们总是可以在字符串的开头返回它。 R要么以大写字符开头,我们可以将其剥离和转换,要么为空。

这现在适用于之前的所有测试,包括长测试:

type A = DashUppercase<'oneTwoThree'> // "one-two-three"
type B = DashUppercase<'OneTwoThree'> // "-one-two-three"
type C = DashUppercase<'23Skidoo'> // "23-skidoo"
type Okay = DashUppercase<'abcdefghijklmnopqrstuvwxyz'> // okay now
type Okay2 = 
  DashUppercase<'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvw'> // also okay

当然,如果你最终有很多大写字符,那么这将达到递归限制,甚至比以前更快,因为有多个条件检查Break

type Oops = DashUppercase<'ABCDEFGHIJKL'> // too long

可以通过在搜索大写字符和搜索非大写字符之间来回切换来避免此限制,但在这一点上,我宁愿尖叫也不愿尝试写下它。特别是因为“非大写字符”列表很长。


所以你去。您可以单独或在小组中解析字符并获得仅适用于相当短的字符串的简单定义,或者搜索分隔符并获得适用于较长字符串的复杂且复杂的定义。

Playground 代码链接

于 2021-07-26T02:06:10.373 回答
1

以下部分:

S extends `${infer START}${UpperAlpha}${infer REST}`

不符合您的预期。

如果您修改功能进行测试:

export type DashUppercase<S extends string> =
  S extends `${infer START}${UpperAlpha}${infer REST}`
    ? START
    : S;

你会看到非常奇怪的结果:

type T1 = DashUppercase<"Two">; // ""
type T2 = DashUppercase<"TwoThree">; // ""
type T3 = DashUppercase<"oneTwoThree">; // "one"
type T4 = DashUppercase<"oneTwoThreeFour">; // "oneTwoThree" | "one"
type X = DashUppercase<"OneTwoThree">; // "" | "One"

当同一个大写字母重复多次时,它按预期工作。但是当涉及不同的字母时,结果将作为数组返回,每个大写字母分别返回(在type Xfor "O"you get""和 for "T"you get的情况下"One",所以"" | "One".

完整的游乐场链接在这里:https ://tsplay.dev/wXkK1W

但是,我不确定如何解决这个问题。:(

于 2021-07-25T21:43:19.603 回答