3
头图

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

This article is translated from the " Template Literal Types " chapter in the TypeScript Handbook.

This article does not strictly follow the original translation, but also explains and supplements part of the content.

Template Literal Types

The template literal type is based on the string literal type , which can be expanded into multiple strings through the joint type.

They have the same syntax as JavaScript template strings, but they can only be used in type operations. When using the template literal type, it will replace the variable 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 combined type, every 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 of joint types, the results will be cross-multiplied. For example, the following example has 2 2 3, a total of 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"

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 (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 such a function makeWatchedObject , it will add a 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):

// 伪代码
const result = makeWatchedObject(baseObject);
result.on(eventName, callBack);

We hope eventName is of the form: attributeInThePassedObject + "Changed" , for example, passedObject has a property firstName , corresponding to the generated eventName is firstNameChanged , Similarly, lastName corresponds lastNameChanged , age corresponds ageChanged .

When the callBack function is called:

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

on() method was initially like this: on(eventName: string, callBack: (newValue: any) => void) . Using such a signature, we cannot achieve the constraints mentioned above. At this time, we can use template literals:

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 "firstNameChanged" , not just "firstName" , and the callback function passes in the value newValue , we want to constrain it to the type string Let's realize the first point first.

In this example, we hope that the type of event name passed in is a union of object attribute names, but each union member is still spliced with a Changed 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 keyof Type ? If we write like this, an error will be reported:

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 message, we can also see the reason for the error. In "Keyof Operator of TypeScript Series" , we know that the keyof operator will return the string | number | symbol , but the type required by the template literal variable is string | number | bigint | boolean | null | undefined . Now, there is one more symbol type, so in fact, we can also write like this:

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

Or write it like 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 we come to realize the second point, the type of the value passed by the callback function is the same as the type of the corresponding attribute value. We are simply on callBack parameters of any type. The key to achieving this constraint lies in the use of generic functions:

  1. Capture the literal of the first parameter of the generic function to generate a literal type
  2. This literal type can be constrained by a combination of object attributes
  3. The type of object property can be accessed through index access
  4. Apply this type to ensure that the parameter type of the callback function and the type of the object property are 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 change on to a generic function.

When a user calls "firstNameChanged" , TypeScript will try to infer the correct type of Key It will match key and "Changed" , and then infer the string "firstName" , and then get firstName attribute, in this example, the type string

Built-in character manipulation types (Intrinsic String Manipulation Types)

Some types of TypeScript can be used for character manipulation. These types are built into the compiler for performance considerations. You can't find them .d.ts

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

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

Technical details of character operation types

Starting from TypeScript 4.1, these built-in functions will 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

The TypeScript series of articles consists of three parts: official document translation, important and difficult analysis, and practical skills. It covers entry, advanced, and actual combat. It aims to provide you with a systematic learning TS tutorial. The entire series is expected to be about 40 articles. Click here to browse the full series of articles, and suggest to bookmark the site by the way.

WeChat: "mqyqingfeng", add me to the only reader group in Kongyu.

If there are mistakes or not rigorous, please correct me, thank you very much. If you like or have some inspiration, star is welcome, which is also an encouragement to the author.


冴羽
9.3k 声望6.3k 粉丝

17 年开始写前端文章,至今 6 个系列,上百篇文章,全网千万阅读