# 模板字面类型

模板字面类型建立在 字符串字面类型 之上,并且能够通过联合扩展成许多字符串。

它们具有与 JavaScript 中的模板字面字符串 相同的语法,但用于类型位置。当与具体字面类型一起使用时,模板字面通过连接内容来生成新的字符串字面类型。

type World = "world";

type Greeting = `hello ${World}`;

当在插值位置使用联合时,类型是可以由每个联合成员表示的每个可能的字符串字面的集合:

type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";

type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;

对于模板字面中的每个插值位置,联合是交叉相乘的:

type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
type Lang = "en" | "ja" | "pt";

type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`;

我们通常建议人们对大型字符串联合使用提前生成,但这在较小的情况下很有用。

# 类型中的字符串联合

当基于类型内的信息定义一个新字符串时,模板字面的力量就来了。

考虑一个函数 (makeWatchedObject) 将一个名为 on()的新函数添加到传递的对象的情况。在 JavaScript 中,它的调用可能类似于:makeWatchedObject(baseObject)。我们可以想象基础对象看起来像:

const passedObject = {
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26,
};

将添加到基础对象的 on函数需要两个参数,一个 eventName(一个 string)和一个 callBack(一个 function)。

eventName的格式应为 attributeInThePassedObject + "Changed";因此,firstNameChanged派生自基础对象中的属性 firstName

调用 callBack函数时:

  • 应该传递与名称 attributeInThePassedObject关联的类型的值;因此,由于 firstName被键入为 string,因此 firstNameChanged事件的回调期望在调用时将 string传递给它。与 age关联的类似事件应该期望使用 number参数调用
  • 应该有 void返回类型(为了演示的简单)

因此,on()的简单函数签名可能是:on(eventName: string, callBack: (newValue: any) => void)。但是,在前面的描述中,我们确定了希望在代码中记录的重要类型约束。模板字面类型让我们将这些约束带入我们的代码中。

declare function makeWatchedObject(obj: any): any;
const person = makeWatchedObject({
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26,
});

// makeWatchedObject has added `on` to the anonymous Object

person.on("firstNameChanged", (newValue) => {
  console.log(`firstName was changed to ${newValue}!`);
});

请注意,on监听事件 "firstNameChanged",而不仅仅是 "firstName"。如果我们要确保符合条件的事件名称集受到观察对象中属性名称的联合的约束,那么我们对 on()的幼稚规范可以变得更加健壮,并在末尾添加 "Changed"。虽然我们很乐意在 JavaScript 中进行这样的计算,即 Object.keys(passedObject).map(x =>${x}Changed),但类型系统中的模板字面提供了类似的字符串操作方法:

type PropEventSource<Type> = {
    on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) => void): void;
};

/// Create a "watched object" with an 'on' method
/// so that you can watch for changes to properties.
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;

有了这个,我们可以构建一些在给定错误属性时出错的东西:

type PropEventSource<Type> = {
    on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) => void): void;
};

declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;
const person = makeWatchedObject({
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26
});

person.on("firstNameChanged", () => {});

// Prevent easy human error (using the key instead of the event name)
person.on("firstName", () => {});

// It's typo-resistant
person.on("frstNameChanged", () => {});

# 使用模板字面进行推理

请注意,我们并没有从原始传递对象中提供的所有信息中受益。给定 firstName的变化(即 firstNameChanged事件),我们应该期望回调将收到 string类型的参数。同样,更改 age的回调应接收 number参数。我们天真地使用 any来输入 callBack的参数。同样,模板字面类型可以确保属性的数据类型与该属性的回调的第一个参数类型相同。

使这成为可能的关键见解是:我们可以使用具有泛型的函数,这样:

  • 第一个参数中使用的字面被捕获为字面类型
  • 该字面类型可以被验证为在泛型中的有效属性的联合中
  • 可以使用 Indexed Access 在泛型结构中查找已验证属性的类型
  • 然后可以应用此类型信息以确保回调函数的参数属于同一类型
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");
    }
})

这里我们把 on变成了一个泛型方法。

当用户使用字符串 "firstNameChanged"调用时,TypeScript 将尝试推断 Key的正确类型。为此,它会将 Key"Changed"之前的内容进行匹配,并推断出字符串 "firstName"。一旦 TypeScript 确定了这一点,on方法就可以在原始对象上获取 firstName的类型,在本例中为 string。同样,当使用 "ageChanged"调用时,TypeScript 会找到属性 age的类型,即 number

推理可以以不同的方式组合,通常是解构字符串,并以不同的方式重构它们。

# 内在字符串操作类型

为了帮助进行字符串操作,TypeScript 包含一组可用于字符串操作的类型。这些类型内置在编译器中以提高性能,在 TypeScript 附带的 .d.ts文件中找不到。

# Uppercase&lt;StringType&gt;

将字符串中的每个字符转换为大写版本。

# 示例

type Greeting = "Hello, world"
type ShoutyGreeting = Uppercase<Greeting>

type ASCIICacheKey<Str extends string> = `ID-${Uppercase<Str>}`
type MainID = ASCIICacheKey<"my_app">

# Lowercase&lt;StringType&gt;

将字符串中的每个字符转换为等效的小写字母。

# 示例

type Greeting = "Hello, world"
type QuietGreeting = Lowercase<Greeting>

type ASCIICacheKey<Str extends string> = `id-${Lowercase<Str>}`
type MainID = ASCIICacheKey<"MY_APP">

# Capitalize&lt;StringType&gt;

将字符串中的第一个字符转换为等效的大写字母。

# 示例

type LowercaseGreeting = "hello, world";
type Greeting = Capitalize<LowercaseGreeting>;

# Uncapitalize&lt;StringType&gt;

将字符串中的第一个字符转换为等效的小写字母。

# 示例

type UppercaseGreeting = "HELLO WORLD";
type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>;
Last Updated: 5/25/2023, 2:35:11 PM