TypeScript 模板字面量类型

模板字面量类型

模板字面量类型以字符串字面量类型为基础,可以通过联合类型拓展成多个字符串。 它们跟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"

如果模板字面量里多个变量都是联合类型,那么可能的结果会交叉相乘,比如下面的例子就是2*2*3=12种结果:

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"

如果真的是非常长的字符串字面量,还是建议提前生成。

类型中的字符串联合类型

模板字面量类型最有用的地方在于你可以根据一个类型内部的信息,定义一个新的字符串,让我们举个例子: 有这样一个函数makeWatchedObject,它会给传入的对象添加一个on方法。我们假设这个对象为:

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

调用时大概是这样的方式:

// 伪代码
const res = makeWatchedObject(passedObject);
res.on('eventName', callBack);

on方法有两个参数,第一个参数eventNamestring类型,第二个参数callBackFunction类型。我们期望eventName的格式可以是传入对象里的key+'Changed'格式:${key}changed,比如firstName应该对应的eventName的值是firstNameChanged。 第二个参数callBack被调用时,传入的参数应该与对象中对应的key的类型相同,比如eventNamefirstNameChanged时,回调函数接收的参数就应该是与firstName的类型相同。 让我们先来实现第一个参数所需要做的约束:

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

function makeWatchedObject<T>(obj: T): T & PropEventSource<T> {
  ...
};

这里使用了string & keyof T,为什么不直接使用keyof T呢?

type PropEventSource<T> = {
  on(eventName: `${keyof T}Changed`, callback: (newValue: any) => void): void;
}
//不能将类型“string | number | symbol”分配给类型“string | number | bigint | boolean | null | undefined”。
//不能将类型“symbol”分配给类型“string | number | bigint | boolean | null | undefined”。

从报错信息中,我们也可以看出报错原因,我们知道keyof操作符会返回string | number | symbol类型,但是模板字面量的变量要求的类型却是string | number | bigint | boolean | null | undefined,比较一下,多了一个 symbol 类型。回到string & keyof T这种写法,我们就可以实现第一个参数的约束了:

const res = makeWatchedObject(passedObject);
res.on('firstNameChanged', () => { });
res.on('fNameChanged', () => { });
res.on('LastfirstName', () => { });
//类型“"fNameChanged"”的参数不能赋给类型“"firstNameChanged" | "lastNameChanged" | "ageChanged"”的参数

现在我们来实现第二个参数的约束,也就是回调函数内的约束,在这里我们借助泛型实现即可:

type PropEventSource<T> = {
  on<Key extends string & keyof T>(eventName: `${Key}Changed`, callback: (newValue: T[Key]) => void): void;
}
function makeWatchedObject<T>(obj: T): T & PropEventSource<T> {
  ...
};
const res = makeWatchedObject(passedObject);
res.on('firstNameChanged', (newValue) => { 
  console.log(newValue); // (parameter) newValue: string
});
res.on('ageChanged', (newValue) => { 
  console.log(newValue); // (parameter) newValue: number
});

内置字符操作类型

TypeScript的一些类型可以用于字符操作,这些类型处于性能的考虑被内置在编译器中,你不能在.d.ts 文件里找到它们。

Uppercase

把每个字符变为大写。

type Greeting1 = 'hello, world';
type ShoutyGreeting = Uppercase<Greeting1>; // type ShoutyGreeting = "HELLO, WORLD"

Lowercase

把每个字符变为小写。

type Greeting2 = 'HELLO, WORLD';
type QuietGreeting = Lowercase<Greeting2>; // type QuietGreeting = "hello, world"

Capitalize

把字符串的第一个字符转为大写。

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

Uncapitalize

把字符串的第一个字符转为小写。

type Greeting4 = 'HELLO, WORLD';
type UncomfortableGreeting  = Uncapitalize<Greeting4>; // type UncomfortableGreeting = "hELLO, WORLD"

字符操作类型的技术细节

从 TypeScript 4.1 起,这些内置函数会直接使用 JavaScript 字符串运行时函数,而不是本地化识别。

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