从4.1开始,TypeScript已经拥有了使用模板字面语法操作和转换字符串的能力。请看下面的例子:
type InternalRoute = `/${string}`
const goToRoute = (route: InternalRoute) => {}
你可以使用任何以 /
开头的元素来调用 goToRoute
。但是任何其他字符串都是错误的。
你可以在模板文字类型中使用联合来扩展成更大的联合:
type EntityAttributes = `${'post' | 'user'}${'Id' | 'Name'}`
// 'postId' | 'userId' | 'postName' | 'userName'
甚至可以在模板文字中使用 infer
。
type GetLastName<TFullName extends string> =
TFullName extends `${infer TFirstName} ${infer TLastName}` ? TLastName : never
这里, ${infer TFirstName} ${infer TLastName}
表示任意两个字符串,中间有一个空格:
Matt Pocock
Jimi Hendrix
Charles Barkley
Emmylou Harris
它实例化 TFirstName
和 TLastName
作为类型变量,如果它匹配传入的字符串,则可以使用它们。 ? TLastName
返回姓氏,这意味着你可以像这样使用 GetLastName
:
type Pocock = GetLastName<'Matt Pocock'>
// "Pocock"
type Hendrix = GetLastName<'Jimi Hendrix'>
// "Hendrix"
type Barkley = GetLastName<'Charles Barkley'>
// "Barkley"
那么更高级的用例呢?如果我们想用破折号替换名称中的空格呢?
type ReplaceSpaceWithDash<TFullName extends string> =
TFullName extends `${infer TFirstName} ${infer TLastName}`
? `${TFirstName}-${TLastName}`
: never
type Name = ReplaceSpaceWithDash<'Emmylou Harris'>
// "Emmylou-Harris"
很好,我们把结果改成 ${TFirstName}-${TLastName}
。现在,我们的类型变量似乎有点命名不当。让我们切换一下:
-
TFullName
toTString
-
TFirstName
toTPrefix
-
TLastName
toTSuffix
type ReplaceSpaceWithDash<TString extends string> =
TString extends `${infer TPrefix} ${infer TSuffix}`
? `${TPrefix}-${TSuffix}`
: never
现在它更通用了。但是还不够泛型——让我们让这个类型助手能够处理用另一个字符串替换任何字符串。
type Replace<
TString extends string,
TToReplace extends string,
TReplacement extends string,
> = TString extends `${infer TPrefix}${TToReplace}${infer TSuffix}`
? `${TPrefix}${TReplacement}${TSuffix}`
: never
我们把 TToReplace
和 -
和 TReplacement
交换了。这最终工作得很好:
type DashName = Replace<'Matt Pocock', ' ', '-'>
// "Matt-Pocock"
除了,有几个bug。例如, never
看起来有点可疑。如果 Replace
没有找到任何 TToReplace
,它返回 never
:
type Result = Replace<'Matt', ' ', '-'>
// never
什么是正确的行为?我们想要返回传入的字符串:
type Replace<
TString extends string,
TToReplace extends string,
TReplacement extends string,
> = TString extends `${infer TPrefix}${TToReplace}${infer TSuffix}`
? `${TPrefix}${TReplacement}${TSuffix}`
: TString
type Result = Replace<'Matt', ' ', '-'>
// "Matt"
第二个缺陷是它只替换一次。如果 TToReplace
有多个实例,则忽略第二个。
type DashCaseName = Replace<'Matt Pocock III', ' ', '-'>
// "Matt-Pocock III"
这似乎是一个难以修复的bug——直到我们考虑 ${infer TPrefix}${TToReplace}${infer TSuffix}
是如何工作的。在像 Matt Pocock III
这样的字符串中,它会这样推断:
-
TPrefix : “Matt”
-
TSuffix
: "Pocock III"
这意味着其余的工作需要在 TSuffix
上执行。再一次,这感觉很棘手——直到我们意识到可以递归地调用类型。这意味着我们可以将 TSuffix
括在 StringReplace
中:
type StringReplace<
TString extends string,
TToReplace extends string,
TReplacement extends string,
> = TString extends `${infer TPrefix}${TToReplace}${infer TSuffix}`
? `${TPrefix}${TReplacement}${StringReplace<
TSuffix,
TToReplace,
TReplacement
>}`
: TString
type Result = StringReplace<'Matt Pocock III', ' ', '-'>
// "Matt-Pocock-III"
当你在做递归的时候,你需要确保你不会陷入一个无限循环。那么让我们来跟踪 StringReplace
传递的内容:
首先, StringReplace<"Matt Pocock III", " ", "-">
。返回 Pocock III
。
第二, StringReplace<"Pocock III", " ", "-">
。返回 III
。
最后, StringReplace<"III", " ", "-">
。因为它找不到 " "
的任何实例,所以它只返回 TString
(在本例中, "III"
)。我们找到了递归循环的终点!
欢迎关注公众号:文本魔术,了解更多