匹配标题大小写的 TypeScript 模板文字类型

TypeScript template literal type to match Title Case

我正在开发一种仅匹配标题大小写字符串的模板文字类型。我想我会先使用 Uppercase<>:

创建一个单词匹配器
const uppercaseWord: Uppercase<string> = 'U';

但它似乎也匹配小写字母:

const uppercaseWord: Uppercase<string> = 'u';

这个不会扔。

我试过参数化它:

type SingleUpperCase<Str extends string> = `${Uppercase<Str>}`;

const upperCaseWord: SingleUpperCase = 'U';

但是无法推断类型参数。它通过显式传递字符串文字来工作:

type SingleUpperCase<Str extends string> = `${Uppercase<Str>}`;

const upperCaseWord: SingleUpperCase<'u'> = 'U';

它有效,但我需要更通用的东西。匹配任何大写字符串的东西。如果我再次尝试将 string 作为类型参数传递,错误已解决,但可以无错误地分配小写字母:

type SingleUpperCase<Str extends string> = `${Uppercase<Str>}`;

const upperCaseWord: SingleUpperCase<string> = 'u'; // no error

我想知道这是否可能,因为它需要来自编译器的 regex-like 字符串匹配。

旁白:我不确定你对“标题大小写”有什么规定;我不知道 "HTML String" 是否在标题中。对于接下来的内容,我假设您只需要确保字符串的第一个字符和每个 space (" ") 之后的第一个字符不是小写。这意味着 "HTML String" 没问题。如果您有不同的规则,您可以调整下面答案中的代码。


TypeScript 中没有表示 title-cased string 的特定类型。 Template literal types don't give this to you; Uppercase<string> and Capitalize<string> yield just string, for want of anything better. For a true title-case string type you would need, as you said, something like regular expression validated string types. There is an open issue at microsoft/TypeScript#41160 询问此类 regex-validated 类型的用例;如果下面的解决方案不能满足您的需求,您可能想就您的用例的问题发表评论,为什么它很有吸引力,以及为什么替代解决方案不够用。


虽然这里没有特定的类型,您可以编写一个recursive模板文字类型TitleCase<T>,它可以用作约束 T。这意味着 T extends TitleCase<T> 当且仅当 T 是一个 title-cased 字符串。

然后,为了避免人们不得不用一些通用类型来注释他们的字符串,你会写一个像 asTitleCase() 这样的辅助函数,它只是 returns 它的输入,但会产生一个编译器如果你传入一个错误的字符串就会出错。

因此,虽然您的理想解决方案如下所示:

/* THIS IS NOT POSSIBLE
const okay: TitleCase = "This Is Fine"; // okay
const error: TitleCase = "This is not fine"; // error
const alsoError: TitleCase = String(Math.random()); // error
*/

可实施的解决方案如下所示:

const okay = asTitleCase("This Is Fine"); // no error
const error = asTitleCase("This is not fine"); // error!
// ---------------------> ~~~~~~~~~~~~~~~~~~
// Argument of type '"This is not fine"' is not assignable to 
// parameter of type '"This Is Not Fine"'.

const alsoError = asTitleCase(String(Math.random())); // error!
// Argument of type 'string' is not assignable to parameter of type
// '"Please Use a Title Cased String Literal Here, Thx"'

同样,这是可实施的,而不是理想的。 title-cased 字符串类型的所有使用都需要获得一个额外的泛型类型参数。

请注意,您可能不需要实际编写 asTitleCase(...) 除非您希望在声明中立即看到错误。大概你有一些关心标题大小写的函数(比如,lookupBookTitle())。如果是这样,您只需使 that 函数通用并在那里强制执行约束。因此,您只需编写 const str = "XXX"; lookupBookTitle(str); 而不是 const str = asTitleCase("XXX"); lookupBookTitle(str);,唯一的区别是错误出现的位置。

此外,在 lookupBookTitle() 之类的实现中,您可能应该将输入扩大到 string 并将其视为已经过验证。尽管 T extends TitleCase<T> 具有对调用者强制约束的效果,但当 T 是未指定的泛型类型参数时,编译器将无法遵循逻辑:

// callers see a function that constrains title to TitleCase
function lookupBookTitle<T extends string>(title: VerifyTitleCase<T>): Book;

// implementer just uses string
function lookupBookTitle(title: string) {  
  const book = db.lookupByTitle(title); 
  if (!book) throw new Error("NO BOOK");
  return book;
}

无论如何,这是实现:

type TitleCase<T extends string, D extends string = " "> =
  string extends T ? never :
  T extends `${infer F}${D}${infer R}` ?
  `${Capitalize<F>}${D}${TitleCase<R, D>}` : Capitalize<T>;

类型TitleCase<T, D>在分隔符D处拆分字符串T,并将每段大写(第一个字符大写)。所以它把一个字符串变成它自己的 title-cased 版本:

type X = TitleCase<"the quick brown fox jumps over the lazy dog.">
// type X = "The Quick Brown Fox Jumps Over The Lazy Dog."

然后我们可以编写一个 VerifyTitleCase<T> 类型来检查是否 T extends TitleCase<T>。如果是这样,它解析为 T。如果不是,它会解析为 TitleCase<T>,或一些 hard-coded 错误字符串,希望让用户知道哪里出了问题。 (根据 microsoft/TypeScript#23689 中的要求,TypeScript 中没有“throw 类型”或“Invalid 类型”;因此使用 hard-coded 错误字符串文字是一种解决方法):

type VerifyTitleCase<T extends string> = T extends TitleCase<T> ? T :
  TitleCase<T> extends never ? "Please Use a Title Cased String Literal Here, Thx" :
  TitleCase<T>

最后,辅助函数:

const asTitleCase = <T extends string>(s: VerifyTitleCase<T>) => s;

Playground link to code