The official TypeScript documentation has been updated for a long time, but the Chinese documentation I could find was still in older versions. Therefore, some new and revised chapters are translated and sorted out.

Template Literal Types Template Literal Types TypeScript Handbook

This paper does not strictly follow the translation of the original text, and some of the content has also been explained and supplemented.

Template Literal Types

Template literal types are based on string literal types and can be extended to multiple strings by union types.

They have the same syntax as JavaScript template strings, but can only be used for type operations. When the template literal type is used, it replaces variables in the template and returns a new string literal:

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

When the variable in the template is a union type, every possible string literal is 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"
Copy the code

If multiple variables in a template literal are of union type, the results are cross-multiplied, as in the following example, which has 12 results:

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

If it is a really long string union type, it is recommended to generate it ahead of time, which 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 information inside a type. Let’s take an example:

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

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

The on method is added to the passed object and takes two arguments, eventName (type string) and callBack (type function) :

/ / pseudo code
const result = makeWatchedObject(baseObject);
result.on(eventName, callBack);
Copy the code

We want the eventName to look like this: AttributeInThePassedObject + “Changed”, for example, passedObject has an attribute firstName, corresponding to the eventName firstNameChanged, in the same way, LastName corresponds to lastNameChanged and age corresponds to ageChanged.

When this callBack function is called:

  • Should be passed in withattributeInThePassedObjectValues of the same type. Such aspassedObject,firstNameIs of typestring, corresponding tofirstNameChangedThe event callback function receives onestringType.ageIs of typenumber, corresponding toageChangedThe event callback function receives onenumberType.
  • The return value type isvoidType.

The signature of the on() method initially looks like this: on(eventName: string, callBack: (newValue: any) => void). We cannot implement these constraints with signatures like this, so we can use template literals instead:

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

Note that in this example, the on method adds an event called “firstNameChanged”, not just “firstName”, and the callback passes the value newValue, which we want to restrict to string. Let’s implement the first one.

In this case, the type of event name we want passed in is a union of object property names, except that each union member also concatenates a Changed character at the end. In JavaScript, we can do this calculation:

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

Template literals provide a similar string operation:

type PropEventSource<Type> = {
    on(eventName: `The ${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>;
Copy the code

Notice that in our example here, in the template literal we’re saying string & keyof Type, can we just say keyof Type? If we write like this, we get 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'.
// ...
Copy the code

From the error message, we can also see an error, in “Keyof operator of TypeScript series”, we know Keyof operator returns the string | number | symbol type, But the template type is a string of literal variable requirements | number | bigint | Boolean | null | undefined, comparison, more than a symbol type, so that we can write like this:

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

Or write:

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

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

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

The Inference with Template Literals

Now let’s implement the second point: the value passed by the callback function is of the same type as the corresponding attribute value. We now simply use any for the argument to callBack. The key to implementing this constraint is to use generic functions:

  1. Generates a literal type by capturing the literal of the first argument to a generic function
  2. This literal type can be constrained by associations of object attributes
  3. The type of an object attribute can be obtained through index access
  4. Apply this type to ensure that the parameter type of the callback function is the same type as the object property 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’ve changed on to a generic function.

When “firstNameChanged” is passed when a user calls, TypeScript tries to infer the correct type of Key. It matches the string before key and “Changed”, inferences the string “firstName”, and then retrieves the type of the original object’s firstName attribute, in this case, string.

Intrinsic String Manipulation Types

Some TypeScript types can be used for character manipulation. These types are built into the compiler for performance reasons, and you can’t find them in.d.ts files.

Uppercase

Uppercase each character:

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

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

Capitalize

Uppercase the first character of a string:

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

Uncapitalize

Converts the first character of a string to lowercase:

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

Technical details of character manipulation types

As of TypeScript 4.1, these built-in functions use JavaScript string runtime functions directly, rather than 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;
}
Copy the code

The TypeScript series

  1. The basics of TypeScript
  2. Common TypeScript Types (Part 1)
  3. Common TypeScript types (part 2)
  4. TypeScript type narrowing
  5. The function of TypeScript
  6. TypeScript object type
  7. The generic of TypeScript
  8. TypeScript’s Keyof operator
  9. TypeScript’s Typeof operator
  10. TypeScript index access types
  11. TypeScript conditional types
  12. TypeScript mapping types

Wechat: “MQyqingfeng”, add me Into Hu Yu’s only readership group.

If there is any mistake or not precise place, please be sure to give correction, thank you very much. If you like or are inspired by it, welcome star and encourage the author.