TypeScript official manual translation plan [Xi]: type control - template literal type

  • Note: at present, there is 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 of TypeScript, I can't guarantee the 100% accuracy of translation. If there are errors, please point them out in the comment area;
  • Translation content: the tentative translation content is TypeScript Handbook , other parts of the translated documents will be supplemented when available;
  • Project address: TypeScript-Doc-Zh , if it helps you, you can click a star~

Official document address of this chapter: Template Literal Types

Template literal type

Template literal type based on String literal type Build, which can be expanded into a variety of strings through union types.

Its syntax and Template string in JavaScript Same, but used to represent types in TypeScript. When used with a specific literal type, the template literal will produce 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 at the interpolation position of the template literal, the resulting type is a collection of string literals that each member of the union type can represent:

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, cross product operation will be performed between the union 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 literals is that they can define a new string based on the existing information in the type.

Suppose there is now a makeWatchedObject function that can add an on method to the incoming object. In JavaScript, the function is called in the form of makeWatchedObject(baseObject). The passed in object parameters are similar to the following:

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

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

The form of eventName is similar to attributeinthepasedobject +'changed '. For example, if the incoming object has a firstName attribute, there will be an eventName called firstNameChanged.

The callBack callback function, when called, will:

  • Accepts a parameter whose type is associated with the type of attributeinthepasedobject. For example, if the type of firstName is string, the callback function corresponding to the event firstNameChanged is expected to accept a string parameter when called. Similarly, the event callback function associated with age should accept a number type parameter when called.
  • The return value type is void (to facilitate the explanation of the example)

The simple version function signature of on() may be as follows: on (eventName: string, callback: (newvalue: any) = > void). However, from the above description, we find that we still need to implement very important type constraints in the code. The template literal type can help us do this.

const person = makeWatchedObject({
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26,
});
 
// The makeWatchedObject function adds an on method to an anonymous object
 
person.on("firstNameChanged", (newValue) => {
  console.log(`firstName was changed to ${newValue}!`);
});

Note that the event on listens to is "firstNameChanged", not "firstName". If we want to ensure that the collection of qualified event names is constrained by the union type of the object property name (with "Changed" at the end), our simple version of 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 method to manipulate strings:

type PropEventSource<Type> = {
    on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) => void): void;
};
 
// Create a listening object with an on method to listen for changes in object properties
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;

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

const person = makeWatchedObject({
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26
});
 
person.on("firstNameChanged", () => {});
 
// Prevent common human errors (wrong use of object property names instead of event names)
person.on("firstName", () => {});
// Argument of type '"firstName"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.
 
// Misspelling
person.on("frstNameChanged", () => {});
// Argument of type '"frstNameChanged"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.

Inference of template literals

Note that so far we have not fully utilized the information provided by the incoming object. When the firstName changes (triggering the firstNameChanged event), we expect the callback function to accept a string type parameter. Similarly, when the age changes, the corresponding callback function will also accept a number type parameter. But at present, we just use any as the type of 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 this is that we can use a function with generics to ensure that:

  1. The literal in the first parameter can be captured as a literal type
  2. Valid properties of the generic form a union type, which verifies that the captured literal type is a member of the union type
  3. You can view the types of validated properties by index access in a generic structure
  4. This type information can be further used 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 turn on into a generic method.

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

Inference has many different combinations, which are usually used to deconstruct strings and reconstruct strings in different ways.

Built in string manipulation type

To facilitate 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>

Converts each character in a 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>

Converts each character in a 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>

Converts the first character of a string to uppercase.

Example:

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

Uncapitalize<StringType>

Converts the first character of a 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 type:

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

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

Keywords: Front-end TypeScript

Added by xtheonex on Sat, 04 Dec 2021 06:12:49 +0200