• Note: There are currently no Chinese translations of the latest TypeScript official documentation available online, so there is a translation plan. Since I’m also a beginner to TypeScript, I can’t guarantee 100% accuracy in TypeScript translations, so I feel free to point out any errors in the comments section.
  • Translation content: The temporary translation content is TypeScript Handbook, and other parts of the translation document will be supplemented later.
  • Project address: typescript-doc-zh, if it helps you, you can click a star ~

Template Literal Types

Template literal types

Template literal types are built on string literal types and can be extended to multiple strings through union types.

It has the same syntax as template strings in JavaScript, but is used in TypeScript to represent types. When used with a concrete literal type, the template literal produces a new string literal type by concatenating the content.

type World = 'world';

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

When union types are used at the interpolation position of template literals, 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"
Copy the code

If the template literal has multiple interpolation positions, the cross product between the union types at each position is performed to get 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"
Copy the code

For large string union types, we recommend that you generate them in advance. For smaller string union types, it can be generated using the method in the example above.

A string union type in a

The power of template literals is their ability to define a new string based on information already in the type.

Suppose you now have a makeWatchedObject function that can add an on method to the passed object. In JavaScript, this function is called like makeWatchedobobject. Where the object parameters passed in look like the following:

const passedObject = {
    firstName: 'Saoirse'.lastName: 'Ronan'.age: 26};Copy the code

The on method to be added to the object takes two arguments, an eventName (string) and a callBack (callBack function).

EventName is similar to the form of attributeInThePassedObject + ‘Changed’. For example, if the incoming object has a firstName attribute, there will be an eventName called firstNameChanged.

The callBack function, when called, will:

  • Accepts a parameter, the type of the parameter andattributeInThePassedObjectType associated with. For instance,firstNameIs of typestring, thenfirstNameChangedThe corresponding callback function of this event is also expected to receive one when it is calledstringType parameter. In the same way, andageThe associated event callback function should accept one when it is callednumberType parameter.
  • The return value type isvoid(For example purposes)

A simplified version of the function signature on() might look like this: on(eventName: string, callBack: (newValue: any) => void). However, from the above description, we see that there are important type constraints that need to be implemented in the code. Template literal types help us do just that.

const person = makeWatchedObject({
  firstName: "Saoirse".lastName: "Ronan".age: 26});// The makeWatchedObject function adds on methods to anonymous objects
 
person.on("firstNameChanged".(newValue) = > {
  console.log(`firstName was changed to ${newValue}! `);
});
Copy the code

Note that on listens for the event “firstNameChanged”, not “firstName”. Our simplified on() method needs further refinement if we want to ensure that the collection of qualified event names is constrained by the union type of object attribute names (with “Changed” at the end). Although we can easily implement this effect in JavaScript, for example using Object.keys(passedObject).map(x => ${x}Changed), template literals in the type system provide a similar way to manipulate strings:

type PropEventSource<Type> = {
    on(eventName: `The ${string & keyof Type}Changed`.callback: (newValue: any) = > void) :void;
};
 
// Create a listener with an ON method to listen for changes in its properties
declare function makeWatchedObject<Type> (obj: Type) :Type & PropEventSource<Type>;
Copy the code

This way, TypeScript throws an error when passing in the wrong argument:

const person = makeWatchedObject({
  firstName: "Saoirse".lastName: "Ronan".age: 26
});
 
person.on("firstNameChanged".() = > {});
 
// Prevent common human errors (incorrectly using the property name of an object instead of the event name)
person.on("firstName".() = > {});
// Argument of type '"firstName"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.
 
// Misspelled
person.on("frstNameChanged".() = > {});
// Argument of type '"frstNameChanged"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" |  "ageChanged"'.
Copy the code

Inference of template literals

Note that so far we have not fully utilized the information provided by the incoming object. We expect the callback to take a string argument when firstName changes (triggering firstNameChanged event). Similarly, when age changes, the corresponding callback takes a number. But for now, we’re just using any as the type of the callback argument. Here we need to use the template literal type again, which ensures that the data type of an attribute is the same as the parameter type of its corresponding callback function.

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

  1. The literals in the first argument can be captured as a literal type
  2. The valid attributes of the generic form a union type, and you can verify that the captured literal type is a member of that union type
  3. The type of the validated property can be viewed in a generic structure through indexed access
  4. This type information can be further utilized to ensure that the arguments to the callback function are 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"); }})Copy the code

Here we make on a generic method.

TypeScript attempts to infer the correct type of the Key when the developer calls the ON method with the string “firstNameChanged”. Specifically, it matches the Key to the part before “Changed” and inferences the string “firstName”. Once TypeScript inference is complete, the ON method can retrieve the type of the original object’s firstName attribute — that is, string. Similarly, when a method is called through “ageChanged”, TypeScript finds that the age property is of type number.

Inference can be combined in a number of different ways, often used to deconstruct strings and refactor strings in different ways.

A built-in string manipulation type

To help manipulate strings, TypeScript introduces related types. To improve performance, these types are built into the compiler and are not found in the.d.ts files that ship 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'
Copy the code

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"
Copy the code

Capitalize<StringType>

Converts the first character of a string to uppercase.

Example:

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

Uncapitalize<StringType>

Converts the first character of a string to lowercase.

Example:

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

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

Starting with TypeScript 4.1, the implementation of these built-in functions operates directly with JavaScript string runtime functions 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;
}
Copy the code