百科狗-知识改变命运!
--

模板字面量类型 - TypeScript 创建类型

梵高12个月前 (11-21)阅读数 18#技术干货
文章标签类型

模板字面量类型

模板字面量类型以字符串字面量类型为基础,可以通过联合类型扩展成多个字符串。

它们跟 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"

如果真的是非常长的字符串联合类型,推荐提前生成,这种还是适用于短一些的情况。


类型中的字符串联合类型(String Unions in Types)

模板字面量最有用的地方在于你可以基于一个类型内部的信息,定义一个新的字符串,让我们举个例子:

有这样一个函数makeWatchedObject,它会给传入的对象添加了一个on方法。在 JavaScript 中,它的调用看起来是这样:makeWatchedObject(baseObject),我们假设这个传入对象为:

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

这个on方法会被添加到这个传入对象上,该方法接受两个参数,eventNamestring类型)和callBackfunction类型):

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

我们希望eventName是这种形式:attributeInThePassedObject +"Changed",举个例子,passedObject有一个属性firstName,对应产生的eventNamefirstNameChanged,同理,lastName对应的是lastNameChangedage对应的是ageChanged

当这个callBack函数被调用的时候:

  • 应该被传入与attributeInThePassedObject相同类型的值。比如passedObject中,firstName的值的类型为string,对应firstNameChanged事件的回调函数,则接受传入一个string类型的值。age的值的类型为number,对应ageChanged事件的回调函数,则接受传入一个number类型的值。
  • 返回值类型为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}!`);
});

注意这个例子里,on方法添加的事件名为"firstNameChanged",而不仅仅是"firstName",而回调函数传入的值newValue,我们希望约束为string类型。我们先实现第一点。

模板字面量类型 - TypeScript 创建类型

在这个例子里,我们希望传入的事件名的类型,是对象属性名的联合,只是每个联合成员都还在最后拼接一个Changed字符,在 JavaScript 中,我们可以做这样一个计算:

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

模板字面量提供了一个相似的字符串操作:

type PropEventSource = {
  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(obj: Type): Type & PropEventSource;

注意,我们在这里例子中,模板字面量里我们写的是string & keyof Type,我们可不可以只写成keyof Type呢?如果我们这样写,会报错:

type PropEventSource = {
  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'.
// ...

从报错信息中,我们也可以看出报错原因,在《TypeScript 系列之 Keyof 操作符》里,我们知道keyof操作符会返回string | number | symbol类型,但是模板字面量的变量要求的类型却是string | number | bigint | boolean | null | undefined,比较一下,多了一个 symbol 类型,所以其实我们也可以这样写:

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

再或者这样写:

type PropEventSource = {
  on(eventName: `${Extract}Changed`, callback: (newValue: any) => void): void;
};

使用这种方式,在我们使用错误的事件名时,TypeScript 会给出报错:

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"'.


模板字面量的推断

现在我们来实现第二点,回调函数传入的值的类型与对应的属性值的类型相同。我们现在只是简单的对callBack的参数使用any类型。实现这个约束的关键在于借助泛型函数:

  1. 捕获泛型函数第一个参数的字面量,生成一个字面量类型
  2. 该字面量类型可以被对象属性构成的联合约束
  3. 对象属性的类型可以通过索引访问获取
  4. 应用此类型,确保回调函数的参数类型与对象属性的类型是同一个类型
type PropEventSource = {
  on
  (eventName: `${Key}Changed`, callback: (newValue: Type[Key]) => void ): void;
};

declare function makeWatchedObject(obj: Type): Type & PropEventSource;

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 

这里我们把on改成了一个泛型函数。

当一个用户调用的时候传入"firstNameChanged",TypeScript 会尝试着推断Key正确的类型。它会匹配key"Changed"前的字符串,然后推断出字符串"firstName",然后再获取原始对象的firstName属性的类型,在这个例子中,就是string类型。


内置字符操作类型

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

Uppercase

把每个字符转为大写形式:

type Greeting = "Hello, world"
type ShoutyGreeting = Uppercase        
// type ShoutyGreeting = "HELLO, WORLD"

type ASCIICacheKey = `ID-${Uppercase}`
type MainID = ASCIICacheKey
// type MainID = "ID-MY_APP"


Lowercase

把每个字符转为小写形式:

type Greeting = "Hello, world"
type QuietGreeting = Lowercase       
// type QuietGreeting = "hello, world"

type ASCIICacheKey = `id-${Lowercase}`
type MainID = ASCIICacheKey    
// type MainID = "id-my_app"


Capitalize

把字符串的第一个字符转为大写形式:

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


Uncapitalize

把字符串的第一个字符转换为小写形式:

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


字符操作类型的技术细节

从 TypeScript 4.1 起,这些内置函数会直接使用 JavaScript 字符串运行时函数,而不是本地化识别(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;
}

鹏仔微信 15129739599 鹏仔QQ344225443 鹏仔前端 pjxi.com 共享博客 sharedbk.com

免责声明:我们致力于保护作者版权,注重分享,当前被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理!邮箱:344225443@qq.com)

图片声明:本站部分配图来自网络。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

内容声明:本文中引用的各种信息及资料(包括但不限于文字、数据、图表及超链接等)均来源于该信息及资料的相关主体(包括但不限于公司、媒体、协会等机构)的官方网站或公开发表的信息。部分内容参考包括:(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供参考使用,不准确地方联系删除处理!本站为非盈利性质站点,本着为中国教育事业出一份力,发布内容不收取任何费用也不接任何广告!)