模板字面量类型
模板字面量类型建立在字符串字面量类型之上,并且能够通过联合类型扩展为许多字符串。
它们与 JavaScript 中的模板字面量字符串具有相同的语法,但用于类型位置。 当与具体的字面量类型一起使用时,模板字面量通过连接内容生成一个新的字符串字面量类型。
当在插值位置使用联合类型时,该类型是由每个联合成员可以表示的每个可能的字符串字面量组成的集合:
type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
Try对于模板字面量中的每个插值位置,联合类型会进行交叉相乘:
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
type Lang = "en" | "ja" | "pt";
type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`;
Try我们通常建议人们对大型字符串联合使用预先生成的方式,但这在较小的情况下很有用。
类型中的字符串联合
模板字面量的强大之处在于根据类型内部的信息定义新的字符串。
考虑这样一种情况:一个函数(makeWatchedObject)向传入的对象添加一个名为 on() 的新函数。在 JavaScript 中,它的调用可能类似于:makeWatchedObject(baseObject)。我们可以想象基础对象如下所示:
将要添加到基础对象的 on 函数期望两个参数:一个 eventName(一个 string)和一个 callback(一个 function)。
eventName 的形式应为 passedObject中的属性名 + "Changed";因此,firstNameChanged 派生自基础对象中的属性 firstName。
callback 函数在被调用时:
- 应该传递一个与名称
passedObject中的属性名相关联的类型的值;因此,由于firstName的类型是string,firstNameChanged事件的回调在调用时期望传递一个string。类似地,与age关联的事件应该期望使用一个number参数调用 - 应该具有
void返回类型(为了简化演示)
因此,on() 的简单函数签名可能是:on(eventName: string, callback: (newValue: any) => void)。然而,在前面的描述中,我们确定了想要在代码中记录的重要类型约束。模板字面量类型让我们能够将这些约束引入到代码中。
const person = makeWatchedObject({
firstName: "Saoirse",
lastName: "Ronan",
age: 26,
});
// makeWatchedObject 已经向匿名对象添加了 `on`
person.on("firstNameChanged", (newValue) => {
console.log(`firstName was changed to ${newValue}!`);
});Try注意 on 监听事件 "firstNameChanged",而不仅仅是 "firstName"。如果我们确保合格事件名称的集合被约束为被观察对象中属性名的联合,并在末尾添加 "Changed",那么我们对 on() 的简单规范可以变得更加健壮。虽然我们很乐意在 JavaScript 中进行这样的计算,例如 Object.keys(passedObject).map(x => `${x}Changed`),但类型系统内部的模板字面量提供了一种类似的字符串操作方法:
type PropEventSource<Type> = {
on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) => void): void;
};
/// 创建一个带有 `on` 方法的“被观察对象”
/// 以便你可以观察属性的变化。
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;Try有了这个,我们可以构建一个在给定错误属性时会报错的东西:
const person = makeWatchedObject({
firstName: "Saoirse",
lastName: "Ronan",
age: 26
});
person.on("firstNameChanged", () => {});
// 防止简单的人为错误(使用键而不是事件名称)
person.on("firstName", () => {});
// 具有防错性
person.on("frstNameChanged", () => {});Try模板字面量的推断
注意,我们并没有从原始传入对象提供的所有信息中受益。考虑到 firstName 的变化(即 firstNameChanged 事件),我们应该期望回调会收到一个 string 类型的参数。类似地,age 变化的回调应该收到一个 number 参数。我们简单地使用 any 来为 callback 的参数进行类型标注。同样,模板字面量类型使得确保属性的数据类型与该属性回调的第一个参数类型相同成为可能。
实现这一点的关键见解是:我们可以使用一个带有泛型的函数,使得:
- 第一个参数中使用的字面量被捕获为字面量类型
- 该字面量类型可以被验证为属于泛型中有效属性的联合
- 可以使用索引访问在泛型结构中查找已验证属性的类型
- 然后可以应用此类型信息以确保回调函数的参数属于同一类型
type PropEventSource<Type> = {
on<Key extends string & keyof Type>
(eventName: `${Key}Changed`, callback: (newValue: Type[Key]) => void): void;
};
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;
const person = 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");
}
})Try这里我们将 on 变成了一个泛型方法。
当用户使用字符串 "firstNameChanged" 调用时,TypeScript 会尝试推断 Key 的正确类型。 为此,它会将 Key 与 "Changed" 之前的内容进行匹配,并推断出字符串 "firstName"。 一旦 TypeScript 弄清楚这一点,on 方法就可以获取原始对象上 firstName 的类型,在这种情况下是 string。 类似地,当使用 "ageChanged" 调用时,TypeScript 会找到属性 age 的类型,即 number。
推断可以以不同的方式组合,通常用于解构字符串,并以不同的方式重构它们。
内置字符串操作类型
为了帮助进行字符串操作,TypeScript 包含一组可用于字符串操作的类型。这些类型内置于编译器中以提高性能,并且不会出现在 TypeScript 附带的 .d.ts 文件中。
Uppercase<StringType>
将字符串中的每个字符转换为大写版本。
示例
type Greeting = "Hello, world"
type ShoutyGreeting = Uppercase<Greeting>
type ASCIICacheKey<Str extends string> = `ID-${Uppercase<Str>}`
type MainID = ASCIICacheKey<"my_app">
TryLowercase<StringType>
将字符串中的每个字符转换为小写版本。
示例
type Greeting = "Hello, world"
type QuietGreeting = Lowercase<Greeting>
type ASCIICacheKey<Str extends string> = `id-${Lowercase<Str>}`
type MainID = ASCIICacheKey<"MY_APP">
TryCapitalize<StringType>
将字符串中的第一个字符转换为大写版本。
示例
Uncapitalize<StringType>
将字符串中的第一个字符转换为小写版本。
示例
type UppercaseGreeting = "HELLO WORLD";
type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>;
Try关于内置字符串操作类型的技术细节
截至 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;
}