• Description : There is currently no Chinese translation of the latest official documents of TypeScript on the Internet, so there is such a translation plan. Because I am also a beginner in TypeScript, I cannot guarantee that the translation will be 100% accurate. If there are errors, please point out in the comment section;
  • translation content : The tentative translation content is TypeScript Handbook , and other parts of the translation document will be added later;
  • project address : TypeScript-Doc-Zh , if it helps you, you can click a star~

The official document address of this chapter: Template Literal Types

Template literal type

The template literal type is constructed based on the string literal type , which can be expanded into a variety of strings through the joint type.

Its syntax is the same as the template string JavaScript, but it is used to represent types in TypeScript. When used with a specific literal type, the template literal will generate a new string literal type by splicing the content.

type World = 'world';

type Greeting = `hello ${World}`;
      ^
     // type Greeting = 'hello world'      

When the union type is used in the interpolation position of the template literal, the final type is a set of string literals that can be represented by each member of the union type:

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"

If the template literal has multiple interpolation positions, the cross product operation will be performed between the joint types at each position to obtain the final type:

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"

For large string union types, we recommend that you generate them in advance. For smaller string union types, you can use the method in the above example to generate.

String union type in type

The power of template literal is that it can define a new string based on the existing information in the type.

Assuming that there is now a makeWatchedObject function, it can add a on method to the incoming object. In JavaScript, the calling form of this function is like: makeWatchedObject(baseObject) . Among them, the incoming object parameters are similar to the following:

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

on method that will be added to the object will accept two parameters, one is eventName (string) and the other is callBack (callback function).

The form of eventName attributeInThePassedObject + 'Changed' . For example, if the incoming object has a firstName attribute, there will be a corresponding firstNameChanged eventName .

The callBack callback function, when called, will:

The function signature of the simplified version of on() on(eventName: string, callBack: (newValue: any) => void) . However, from the above description, we found that the code still needs to implement very important type constraints. The template literal type just can help us do this.

const person = makeWatchedObject({
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26,
});
 
// makeWatchedObject 函数给匿名对象添加了 on 方法
 
person.on("firstNameChanged", (newValue) => {
  console.log(`firstName was changed to ${newValue}!`);
});

Note, on listening event is "firstNameChanged" , rather than "firstName" . If we want to ensure that the set of eligible event names is constrained by the union type of the object attribute name (with "Changed" at the end), then our simplified version on() method needs to be further improved. Although we can easily achieve this effect in JavaScript, such as using Object.keys(passedObject).map(x => ${x}Changed) , the template literal in the type system also provides a similar way to manipulate strings:

type PropEventSource<Type> = {
    on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) => void): void;
};
 
// 创建一个带有 on 方法的监听对象,从而监听对象属性的变化
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;

In this way, when the wrong parameter is passed in, TypeScript will throw an error:

const person = makeWatchedObject({
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26
});
 
person.on("firstNameChanged", () => {});
 
// 预防常见的人为错误(错误地使用了对象的属性名而不是事件名)
person.on("firstName", () => {});
// Argument of type '"firstName"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.
 
// 拼写错误
person.on("frstNameChanged", () => {});
// Argument of type '"frstNameChanged"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.

Inference of template literal

Note that so far we have not fully utilized the information provided by the incoming object. firstName changes (triggering the firstNameChanged event), we expect the callback function to accept a string type 061b2fafdbd952. Similarly, when age changes, the corresponding callback function will also accept a number type 061b2fafdbd956. But at present, we just use any as the type of the callback function parameter. Here we need to use the template literal type again, which can ensure that the data type of the attribute is consistent with the parameter type of the callback function corresponding to the attribute.

The key to achieving this is: we can use a function with generics to ensure:

  1. The literal in the first parameter can be captured as a literal type
  2. The valid attributes of the generic type will constitute a union type, which can verify whether the captured literal type is a member of the union type
  3. You can view the type of verified attributes in the generic structure by accessing by index
  4. The type information can be further utilized to ensure that the parameters of the callback function are also of the same type
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");
    }
})

Here we make on a generic method.

When the developer calls the on "firstNameChanged" , TypeScript will try to infer the correct type Key Specifically, it will match the previous part of Key and "Changed" "firstName" . Once the TypeScript inference is complete, the on method can retrieve firstName attribute of the original object-that is, the string type. Similarly, when by "ageChanged" when calling a method, TypeScript will find age type of property is number .

It is inferred that there are many different combinations, which are usually used to deconstruct strings and reconstruct the strings in different ways.

Built-in string manipulation type

In order to facilitate the manipulation of strings, TypeScript introduces some related types. To improve performance, these types are built into the compiler and cannot be found in the .d.ts file that comes with TypeScript.

Uppercase<StringType>

Convert each character in the string to uppercase.

Example:

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'

Lowercase<StringType>

Convert each character in the string to lowercase.

Example:

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"

Capitalize<StringType>

Convert the first character of the string to uppercase.

Example:

type LowercaseGreeting = 'hello, world';
type Greeting = Capitalize<LowercaseGreeting>;
        ^
       // type Greeting = 'Hello, world'     

Uncapitalize<StringType>

Convert the first character of the string to lowercase.

Example:

type UppercaseGreeting = 'HELLO WORLD';
type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>;
           ^    
          // type UncomfortableGreeting = "hELLO WORLD"     

Some technical details about the built-in string manipulation types:

Starting from TypeScript 4.1, the implementation of these built-in functions directly uses JavaScript string runtime functions for operations, and cannot be localized.

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;
}

Chor
2k 声望5.9k 粉丝