TypeScript从类型创建类型之模板字面量类型

220 阅读2分钟

模板字符类型(Template literal types)基于字符字面量类型,并且可以通过联合扩展成许多字符串。 他们和JavaScript字符模板有相同的语法,但是作用不同。当与具体文字一起使用时,通过连接内容,一个模板字面量可以产生一个新的字面量

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

联合插值的位置被使用的时候,类型是由每个联合类型标识的每个可能的字符串文字的集合。

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"

模板字面量的每个插值的位置,联合类型会被交叉相乘

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"

在类型中使用字符联合(String Uniond in Types)

当基于类型内的信息定义一个新字符串时,模板字面量的好处就体现出来了。 考虑实现一个功能,让makeWatchedObject函数为传入的对象添加一个on()函数。在JavaScript中,类似的形式就是:makeWatchedObject(baseObject)。我们可以把基础的对象看作是:

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

on函数将被添加到passedObject上,并传递两个参数:eventNamecallBackeventNamepassedObject的属性+'Changed'组成。因此,firstNameChanged在基础对象的firstName属性衍生出来。

callBack被调用时:

  1. passedObject的属性被复制;因此,由于firstNamestring类型,那么当调用它时,firstNameChanged函数希望传递一个string类型的值。age类似。
  2. 应该返回void

on函数的==调用签名==看起来应该是:on(eventName: string, callBack: (newValue: any) => void)。实际上,在前一个描述当中,我们确定了在代码中的重要类型约束模板字面量类型解决了我们需要的==约束方式==。

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

请注意onfirstNameChanged上监听,而不是firstName。假如我们可以确保监听的属性在结尾添加了Changed,我们的初级on可以变得更加强大。虽然我们可以在JavaScript中这么写:==Object.keys(passedObject).map(x => `${x}Changed)`==,模板字面量可以实现一个类似的方式:

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

这样,我们就可以在赋予错误的属性时,给出错误:

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 Tempalte Literals)

firstName的变成了firstNameChanged,但是我们同时想让firstNameChanged函数接受string类型的参数。类似的,ageageChanged应该接受一个number类型的参数。我们可能自然的想到用any来作为callBack的参数类型。不过,模板字符类型可以实现属性的回调函数的第一个参数的类型和属性的类型保持一致。 我们敏锐的察觉到:我们可以使用一个带有泛型的函数:

  1. 第一个参数被捕获为字面量类型
  2. 这个文字类型可以被校验,确保一定在泛型的属性的联合类型中
  3. 已经被验证的属性的类型可以使用索引访问的方式在泛型中找到
  4. 之后可以应用这个类型信息确保和回调函数的参数属于同一类型
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");
    }
})

这里让on变成了一个泛型方法。 当调用firstNameChanged时,TypeScript将尝试为Key推导正确的类型。为了达到这个目的,TypeScript会把KeyChanged之前的内容进行匹配,并推导出字符串firstNameon方法可以拿到firstName的类型。类似的"ageChanged",TypeScript会把agenumber类型赋予"ageChanged"的参数。

推理可以以不同的方式组合,通常是结构字符串,并以不同的方式重构它们。

内置的字符串操作类型(Intrinsic String Manipulation Types)

为了操纵string类型,TypeScript包含了一些列类型的设置可以被用在类型操作上。这些类型内置于编译器以提高性能,在TypeScript包含的.d.ts文件中找不到。

Uppercase<StringType>

转换每个字符成为大写

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"

Lowercase<StringType>

转换每个字符成为小写

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"

Capitalize<StringType>

转换第一个字符成为大写Example

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

Uncapitalize<StringType>

转换第一个字符成为小写

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

翻译自 TypeScript: Documentation - Template Literal Types (typescriptlang.org))