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 examplepassedObject
in,firstName
type of value isstring
, the correspondingfirstNameChanged
event callback function, then accept incoming astring
type of value.age
type of the value ofnumber
, the correspondingageChanged
event callback function, passing it to accept anumber
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:
- Capture the literal of the first parameter of the generic function to generate a literal type
- This literal type can be constrained by a combination of object attributes
- The type of object property can be accessed through index access
- 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.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。