Template literal type of TypeScript

The official documents of TypeScript have already been updated, but the Chinese documents I can find are still in the older version. Therefore, some newly added and revised chapters are translated and sorted out.

This translation is compiled from "TypeScript Handbook" Template Literal Types "Chapter.

This article is not translated strictly according to the original text, and some contents are explained and supplemented.

Template Literal Types

Template literal type String literal type Based on, it can be extended to multiple strings through union types.

They have the same syntax as JavaScript Template Strings, but can only be used in type operations. When the template literal type is used, it will replace the variables in the template and return a new string literal:

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

When the variable in the template is a union type, each possible string literal will be represented:

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 multiple variables in the template literal are union types, the results will be cross multiplied. For example, the following example has 12 results in total:

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"

If it is really a very long string union type, it is recommended to generate it in advance. This is still suitable for shorter cases.

String Unions in Types

The most useful thing about template literals is that you can define a new string based on the internal information of a type. Let's take an example:

There is a function makeWatchedObject, which adds an on method to the incoming object. In JavaScript, its call looks like this: makeWatchedObject(baseObject). We assume that the incoming object is:

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

The on method will be added to the incoming object. The method accepts two parameters, eventName (string type) and callBack (function type):

// Pseudo code
const result = makeWatchedObject(baseObject);
result.on(eventName, callBack);

We want the eventName to be in this form: attributeInThePassedObject + "Changed". For example, the passedObject has an attribute firstName, and the corresponding eventName is firstNameChanged. Similarly, lastName corresponds to lastNameChanged, and age corresponds to ageChanged.

When this callBack function is called:

  • Should be passed in a value of the same type as attributeInThePassedObject. For example, in passedObject, the type of the value of firstName is string, and the callback function corresponding to the firstNameChanged event accepts a value of string type. The type of the value of age is number. The callback function corresponding to the ageChanged event accepts a value of type number.
  • The return value type is void.

The signature of the on() method starts with this: on (eventName: string, callback: (newvalue: any) = > void). Using such a signature, we cannot implement the above constraints. At this time, we can use the template literal:

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

Note that in this example, the event name added by the on method is "firstNameChanged", not just "firstName", and the value newValue passed in by the callback function is expected to be constrained to string type. Let's realize the first point first.

In this example, we hope that the type of the passed in event name is the union of object attribute names, but each union member is still spliced with a Changed character at the end. In JavaScript, we can do such a calculation:

Object.keys(passedObject).map(x => ${x}Changed)

Template literals provide a similar string operation:

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

Note that in our example, we write string & keyof Type in the template literal. Can we just write it as keyof Type? If we write this, we will report an error:

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

// Type 'keyof Type' is not assignable to type 'string | number | bigint | boolean | null | undefined'.
// Type 'string | number | symbol' is not assignable to type 'string | number | bigint | boolean | null | undefined'.
// ...

From the error reporting information, we can also see the reasons for the error reporting. In Keyof operator of TypeScript series In, we know that the keyof operator will return the type of string | number | symbol, but the type required by the literal variable of the template is string | number | bigint | boolean | null | undefined. By comparison, there is an additional symbol type, so we can write it like this:

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

Or write this:

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

In this way, when we use the wrong event name, TypeScript will give an error:

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"'.
 
// It's typo-resistant
person.on("frstNameChanged", () => {});
// Argument of type '"frstNameChanged"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.

Inference with Template Literals

Now let's implement the second point. The type of value passed in by the callBack function is the same as that of the corresponding attribute value. We are now simply using the any type for the callBack parameter. The key to implementing this constraint is to use generic functions:

  1. Capture the literal of the first parameter of a generic function and generate a literal type
  2. The literal type can be constrained by the union of object properties
  3. The type of object property can be obtained through index access
  4. Apply this type to ensure that the parameter type of the callback function is the same as the type of the object property
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 change on to a generic function.

When a user calls "firstNameChanged", TypeScript will try to infer the correct type of key. It will match the string before key and "Changed", infer the string "firstName", and then obtain the type of the firstName property of the original object. In this example, it is the string type.

Built in character manipulation types

Some TypeScript types can be used for character operations. These types are built into the compiler for performance reasons. You can't find them in the. d.ts file.

Uppercase<StringType>

Convert each character to uppercase:

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 to lowercase:

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:

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

Uncapitalize<StringType>

Convert the first character of a string to lowercase:

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

Technical details of character operation type

Since TypeScript 4.1, these built-in functions directly use JavaScript string runtime functions instead of locale aware.

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

TypeScript series

  1. Introduction to the basics of TypeScript
  2. Common types of TypeScript (I)
  3. Common types of TypeScript (Part 2)
  4. TypeScript type narrowing
  5. Functions of TypeScript
  6. Object type of TypeScript
  7. Generics of TypeScript
  8. Keyof operator of TypeScript
  9. Typeof operator of TypeScript
  10. Index access type of TypeScript
  11. Condition type of TypeScript
  12. Mapping type of TypeScript

Wechat: "mqyqingfeng", add me to Yu Yu's only reader group.

If there is any mistake or lack of preciseness, please be sure to correct it. Thank you very much. If you like it or have some inspiration, welcome star, which is also an encouragement to the author.

Keywords: Javascript Front-end html5 TypeScript

Added by selsdon on Wed, 08 Dec 2021 02:50:43 +0200