模板字面量类型基于 字符串字面量类型,并具有通过联合类型扩展为多种字符串的能力。
它们的语法与 JavaScript 中的模板字面量字符串 相同,但用于类型位置。当与具体的字面量类型一起使用时,模板字面量通过拼接内容产生一个新的字符串字面量类型。
tsTrytypeWorld = "world";typeGreeting = `hello ${World }`;
当联合类型用于插值位置时,该类型将是每个联合成员所能代表的每一个可能的字符串字面量的集合。
tsTrytypeEmailLocaleIDs = "welcome_email" | "email_heading";typeFooterLocaleIDs = "footer_title" | "footer_sendoff";typeAllLocaleIDs = `${EmailLocaleIDs |FooterLocaleIDs }_id`;
对于模板字面量中的每个插值位置,联合类型都会进行交叉相乘(笛卡尔积)。
tsTrytypeAllLocaleIDs = `${EmailLocaleIDs |FooterLocaleIDs }_id`;typeLang = "en" | "ja" | "pt";typeLocaleMessageIDs = `${Lang }_${AllLocaleIDs }`;
我们通常建议开发者在处理大型字符串联合类型时使用提前生成的方法,但这在较小的情况下非常有用。
类型中的字符串联合
模板字面量的强大之处在于根据类型内部的信息定义新字符串。
考虑这样一种情况:一个函数(makeWatchedObject)向传入的对象添加一个名为 on() 的新函数。在 JavaScript 中,它的调用可能看起来像:makeWatchedObject(baseObject)。我们可以想象基础对象看起来是这样的:
tsTryconstpassedObject = {firstName : "Saoirse",lastName : "Ronan",age : 26,};
将要添加到基础对象中的 on 函数需要两个参数,一个 eventName(字符串 string)和一个 callback(函数 function)。
eventName 应该具有 传入对象中的属性名 + "Changed" 的形式;因此,它是从基础对象的属性 firstName 派生出的 firstNameChanged。
callback 函数在被调用时:
- 应该传入一个与
传入对象中的属性名关联的类型值;因此,由于firstName被定义为string类型,firstNameChanged事件的回调函数在调用时应该期望接收一个string。类似地,与age关联的事件应该期望接收一个number参数。 - 应该具有
void返回类型(为了演示简单起见)。
on() 的简单函数签名可能是:on(eventName: string, callback: (newValue: any) => void)。然而,在前面的描述中,我们确定了想要在代码中记录的重要类型约束。模板字面量类型让我们能够将这些约束带入代码中。
tsTryconstperson =makeWatchedObject ({firstName : "Saoirse",lastName : "Ronan",age : 26,});// makeWatchedObject has added `on` to the anonymous Objectperson .on ("firstNameChanged", (newValue ) => {console .log (`firstName was changed to ${newValue }!`);});
请注意,on 监听的是 "firstNameChanged" 事件,而不仅仅是 "firstName"。如果我们能确保合格的事件名称集合被限制为“被监视对象中的属性名加上后缀‘Changed’”构成的联合类型,那么我们对 on() 的简单定义会变得更加健壮。虽然我们在 JavaScript 中可以很轻松地进行此类计算,即 Object.keys(passedObject).map(x => `${x}Changed`),但在 类型系统内部 使用模板字面量提供了类似的字符串操作方法。
tsTrytypePropEventSource <Type > = {on (eventName : `${string & keyofType }Changed`,callback : (newValue : any) => void): void;};/// Create a "watched object" with an `on` method/// so that you can watch for changes to properties.declare functionmakeWatchedObject <Type >(obj :Type ):Type &PropEventSource <Type >;
有了这个,我们可以构建一个在给定错误属性时会报错的东西。
tsTryconstperson =makeWatchedObject ({firstName : "Saoirse",lastName : "Ronan",age : 26});person .on ("firstNameChanged", () => {});// Prevent easy human error (using the key instead of the event name)Argument of type '"firstName"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.2345Argument of type '"firstName"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.person .on ("firstName" , () => {});// It's typo-resistantArgument of type '"frstNameChanged"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.2345Argument of type '"frstNameChanged"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.person .on ("frstNameChanged" , () => {});
模板字面量类型推断
请注意,我们并没有利用原始传入对象中提供的所有信息。鉴于 firstName 的变化(即 firstNameChanged 事件),我们应该期望回调函数接收一个 string 类型的参数。同样地,age 变化的回调函数应该接收一个 number 参数。我们天真地使用了 any 来标注 callback 的参数。同样地,模板字面量类型使我们能够确保属性的数据类型与其回调函数的第一个参数类型一致。
实现这一点的关键洞察是:我们可以使用一个带有泛型的函数,使得:
- 用作第一个参数的字面量被捕获为字面量类型。
- 该字面量类型可以被验证为存在于泛型中有效属性的联合类型内。
- 可以使用索引访问(Indexed Access)在泛型的结构中查找已验证属性的类型。
- 然后,可以将此类型信息应用于确保回调函数的参数具有相同的类型。
tsTrytypePropEventSource <Type > = {on <Key extends string & keyofType >(eventName : `${Key }Changed`,callback : (newValue :Type [Key ]) => void): void;};declare functionmakeWatchedObject <Type >(obj :Type ):Type &PropEventSource <Type >;constperson =makeWatchedObject ({firstName : "Saoirse",lastName : "Ronan",age : 26});person .on ("firstNameChanged",newName => {console .log (`new name is ${newName .toUpperCase ()}`);});person .on ("ageChanged",newAge => {if (newAge < 0) {console .warn ("warning! negative age");}})
这里我们将 on 做成了一个泛型方法。
当用户使用字符串 "firstNameChanged" 调用时,TypeScript 会尝试为 Key 推断正确的类型。为了做到这一点,它会将 Key 与 "Changed" 之前的内容进行匹配,并推断出字符串 "firstName"。一旦 TypeScript 弄清楚了这一点,on 方法就可以获取原始对象上 firstName 的类型,在这种情况下是 string。类似地,当使用 "ageChanged" 调用时,TypeScript 会找到属性 age 的类型,即 number。
类型推断可以以不同的方式组合,通常用于解构字符串,并以不同的方式重建它们。
内置字符串操作类型
为了辅助字符串操作,TypeScript 包含了一组可用于字符串操作的类型。这些类型为了性能是编译器内置的,不能在 TypeScript 附带的 .d.ts 文件中找到。
Uppercase<StringType>
将字符串中的每个字符转换为大写版本。
示例
tsTrytypeGreeting = "Hello, world"typeShoutyGreeting =Uppercase <Greeting >typeASCIICacheKey <Str extends string> = `ID-${Uppercase <Str >}`typeMainID =ASCIICacheKey <"my_app">
Lowercase<StringType>
将字符串中的每个字符转换为小写版本。
示例
tsTrytypeGreeting = "Hello, world"typeQuietGreeting =Lowercase <Greeting >typeASCIICacheKey <Str extends string> = `id-${Lowercase <Str >}`typeMainID =ASCIICacheKey <"MY_APP">
Capitalize<StringType>
将字符串的第一个字符转换为大写版本。
示例
tsTrytypeLowercaseGreeting = "hello, world";typeGreeting =Capitalize <LowercaseGreeting >;
Uncapitalize<StringType>
将字符串的第一个字符转换为小写版本。
示例
tsTrytypeUppercaseGreeting = "HELLO WORLD";typeUncomfortableGreeting =Uncapitalize <UppercaseGreeting >;
关于内置字符串操作类型的技术细节
截至 TypeScript 4.1,这些内置函数的代码直接使用 JavaScript 运行时字符串函数进行操作,且不感知本地化。
function applyStringMapping(symbol: Symbol, str: string) {
switch (intrinsicTypeKinds.get(symbol.escapedName as string)) {
case IntrinsicTypeKind.Uppercase: return str.toUpperCase();
case IntrinsicTypeKind.Lowercase: return str.toLowerCase();
case IntrinsicTypeKind.Capitalize: return str.charAt(0).toUpperCase() + str.slice(1);
case IntrinsicTypeKind.Uncapitalize: return str.charAt(0).toLowerCase() + str.slice(1);
}
return str;
}