模板字面类型

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

¥Template literal types build on string literal types, and have the ability to expand into many strings via unions.

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

¥They have the same syntax as template literal strings in JavaScript, but are used in type positions. When used with concrete literal types, a template literal produces a new string literal type by concatenating the contents.

ts
type World = "world";
 
type Greeting = `hello ${World}`;
type Greeting = "hello world"
Try

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

¥When a union is used in the interpolated position, the type is the set of every possible string literal that could be represented by each union member:

ts
type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
 
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
type AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"
Try

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

¥For each interpolated position in the template literal, the unions are cross multiplied:

ts
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
type Lang = "en" | "ja" | "pt";
 
type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`;
type LocaleMessageIDs = "en_welcome_email_id" | "en_email_heading_id" | "en_footer_title_id" | "en_footer_sendoff_id" | "ja_welcome_email_id" | "ja_email_heading_id" | "ja_footer_title_id" | "ja_footer_sendoff_id" | "pt_welcome_email_id" | "pt_email_heading_id" | "pt_footer_title_id" | "pt_footer_sendoff_id"
Try

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

¥We generally recommend that people use ahead-of-time generation for large string unions, but this is useful in smaller cases.

类型中的字符串联合

¥String Unions in Types

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

¥The power in template literals comes when defining a new string based on information inside a type.

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

¥Consider the case where a function (makeWatchedObject) adds a new function called on() to a passed object. In JavaScript, its call might look like: makeWatchedObject(baseObject). We can imagine the base object as looking like:

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

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

¥The on function that will be added to the base object expects two arguments, an eventName (a string) and a callback (a function).

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

¥The eventName should be of the form attributeInThePassedObject + "Changed"; thus, firstNameChanged as derived from the attribute firstName in the base object.

调用 callback 函数时:

¥The callback function, when called:

  • 应传递与名称 attributeInThePassedObject 关联的类型的值;因此,由于 firstName 被键入为 stringfirstNameChanged 事件的回调期望在调用时将 string 传递给它。与 age 关联的类似事件应该期望使用 number 参数调用

    ¥Should be passed a value of the type associated with the name attributeInThePassedObject; thus, since firstName is typed as string, the callback for the firstNameChanged event expects a string to be passed to it at call time. Similarly events associated with age should expect to be called with a number argument

  • 应该有 void 返回类型(为了演示的简单)

    ¥Should have void return type (for simplicity of demonstration)

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

¥The naive function signature of on() might thus be: on(eventName: string, callback: (newValue: any) => void). However, in the preceding description, we identified important type constraints that we’d like to document in our code. Template Literal types let us bring these constraints into our code.

ts
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}!`);
});
Try

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

¥Notice that on listens on the event "firstNameChanged", not just "firstName". Our naive specification of on() could be made more robust if we were to ensure that the set of eligible event names was constrained by the union of attribute names in the watched object with “Changed” added at the end. While we are comfortable with doing such a calculation in JavaScript i.e. Object.keys(passedObject).map(x => `${x}Changed`), template literals inside the type system provide a similar approach to string manipulation:

ts
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>;
Try

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

¥With this, we can build something that errors when given the wrong property:

ts
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", () => {});
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"'.
 
// It's typo-resistant
person.on("frstNameChanged", () => {});
Argument 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"'.
Try

使用模板字面进行推断

¥Inference with Template Literals

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

¥Notice that we did not benefit from all the information provided in the original passed object. Given change of a firstName (i.e. a firstNameChanged event), we should expect that the callback will receive an argument of type string. Similarly, the callback for a change to age should receive a number argument. We’re naively using any to type the callback’s argument. Again, template literal types make it possible to ensure an attribute’s data type will be the same type as that attribute’s callback’s first argument.

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

¥The key insight that makes this possible is this: we can use a function with a generic such that:

  1. 第一个参数中使用的字面被捕获为字面类型

    ¥The literal used in the first argument is captured as a literal type

  2. 该字面类型可以被验证为在泛型中的有效属性的联合中

    ¥That literal type can be validated as being in the union of valid attributes in the generic

  3. 可以使用 Indexed Access 在泛型结构中查找已验证属性的类型

    ¥The type of the validated attribute can be looked up in the generic’s structure using Indexed Access

  4. 然后可以应用此类型信息以确保回调函数的参数属于同一类型

    ¥This typing information can then be applied to ensure the argument to the callback function is of the same type

ts
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 => {
(parameter) newName: string
console.log(`new name is ${newName.toUpperCase()}`);
});
 
person.on("ageChanged", newAge => {
(parameter) newAge: number
if (newAge < 0) {
console.warn("warning! negative age");
}
})
Try

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

¥Here we made on into a generic method.

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

¥When a user calls with the string "firstNameChanged", TypeScript will try to infer the right type for Key. To do that, it will match Key against the content before "Changed" and infer the string "firstName". Once TypeScript figures that out, the on method can fetch the type of firstName on the original object, which is string in this case. Similarly, when called with "ageChanged", TypeScript finds the type for the property age which is number.

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

¥Inference can be combined in different ways, often to deconstruct strings, and reconstruct them in different ways.

内在字符串操作类型

¥Intrinsic String Manipulation Types

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

¥To help with string manipulation, TypeScript includes a set of types which can be used in string manipulation. These types come built-in to the compiler for performance and can’t be found in the .d.ts files included with TypeScript.

Uppercase<StringType>

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

¥Converts each character in the string to the uppercase version.

示例

¥Example

ts
type Greeting = "Hello, world"
type ShoutyGreeting = Uppercase<Greeting>
type ShoutyGreeting = "HELLO, WORLD"
 
type ASCIICacheKey<Str extends string> = `ID-${Uppercase<Str>}`
type MainID = ASCIICacheKey<"my_app">
type MainID = "ID-MY_APP"
Try

Lowercase<StringType>

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

¥Converts each character in the string to the lowercase equivalent.

示例

¥Example

ts
type Greeting = "Hello, world"
type QuietGreeting = Lowercase<Greeting>
type QuietGreeting = "hello, world"
 
type ASCIICacheKey<Str extends string> = `id-${Lowercase<Str>}`
type MainID = ASCIICacheKey<"MY_APP">
type MainID = "id-my_app"
Try

Capitalize<StringType>

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

¥Converts the first character in the string to an uppercase equivalent.

示例

¥Example

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

Uncapitalize<StringType>

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

¥Converts the first character in the string to a lowercase equivalent.

示例

¥Example

ts
type UppercaseGreeting = "HELLO WORLD";
type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>;
type UncomfortableGreeting = "hELLO WORLD"
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;
}