头图

引言

本文主要介绍模板 TypeScript 对模板字符串的能力支持及实现原理,深入介绍了 TypeScript 提供的字符串操作能力。结合《TypeScript玩转类型操作之基础篇》内容,完整覆盖了TS体操的能力基础。附带一步步实现将 aa_bb___cc 转换成 aaBbCc 的类型实现技巧。

模板字符串

模板字符串是 JS 语言提供了使用反引号 ` 分割的字面量,支持多行字符、嵌入字符串插值表达式以及附带标签(与标签函数异同使用)。

let str = `user name is : ${name}`; // 插值表达式

let str2 = `
line1....
line2....
` ; // 多行字符

function str3Tag(all, ...expresitons) {
  console.log(all, expresitons); 
  return all
}

const name = 'tony';
const age = 11;

str3Tag`hello name is: ${str}, age is ${age}`)

// all: ['hello name is: ', ', age is ', '', raw: Array(3)]
//expressions: Array(0) (2) ['name', 11]

模板字符串在JS中的应用不是本文我们的重点,如果大家想深入了解一下可以先看看MDN然后再翻翻ECMAScript的规范基本上就很深入了。主要是介绍在 TypeScript 中,支持了哪些特性,提供了什么能力。

TS 中模板字符串的相关特性

当模板字符串插值表达式用于类型位置时,具有以下特性:计算出来的模板字符串值会作为一个字符串固定类型、如果插值表达式是一个联合类型那么将得到一个新的联合类型成员是插值联合类型成员与模板字符串静态部分生成字符串并集。

构造新的类型

普通插值表达式,构造一个复合型的字符串类型。

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 = 
    | "ch_welcome_email_id" 
     | "ch_email_heading_id" 
  | "ch_footer_title_id" 
  | "ch_footer_sendoff_id" 
  | "en_welcome_email_id" 
  | "en_email_heading_id" 
  | "en_footer_title_id" 
  | "en_footer_sendoff_id" 
  | "jp_welcome_email_id" 
  | "jp_email_heading_id" 
  | "jp_footer_title_id" 
  | "jp_footer_sendoff_id"
*/

TS 会动态遍历插值中的联合类型,然后构造出所有情况下组合出来的并集联合类型。这在我们项目中组合出新的类型挺实用。

使用模板字符串进行推理

基于对象类型内部属性名构造字符串类型,然后根据对象类型中对应的属性值类型进行推导新的类型。功能会很强大。我们以一个案例进行解释:题目是对一个对象进行代理,代理后需要监听到这个对象的变化事件,现在需要定义一套类型来实现这个功能。

简化的JS代码如下:

const tonyDefaultInfo = {
  name: 'tony',
  age: 11,
};

function makeWatchedObject(target) {
  // 代理处理 ... 
  return target;
}

const tony = makeWatchedObject(tonyDefaultInfo);

tony.on('nameChanged', (newValue) => {
    console.log(newValue);
});

现在我们需要使用类型来对 调用 on 上注册的事件名称做检查:要求on监听的属性名称必须满足(属性名+ 'Changed' ) 格式,监听函数返回值类型和原对象一致【监听name,返回值类型是 string、监听age返回值类型是number】。我们分三步实现一下这个类型的定义。

第一步:如何修饰 makeWatchedObject 函数的类型,定义其返回值类型为传入的值类型。

declare function makeWatchedObject<Type>(obj: Type): Type & { on(eventName: string, callback:(newValue: any)=>void):void };

第二步:定义一个类型工具,负责提取类型上的属性作为模板字符串的插值。大家不清楚 extends 、 keyof 用法的请看这篇文章 《TypeScript 玩转类型操作之基础篇》,里面详细介绍了其使用场景以及方式,包含与 typeof、infer 等关键词的配合使用以及Omit等高级类型的源码解析。

type PickPropertyNameComposeChange<T extends {[k:string]:any}> = {
  [K in keyof T]: `${K}Changed`;
}

type Person = {
  name: string;
  age: number
}

type ChangeKeys = PickPropertyNameComposeChange<Person>

/*
type ChangeKeys = {
    name: "nameChanged";
    age: "ageChanged";
}
*/

第三步:将属性名提取封装成字符串用于on函数参数的修饰, 并提取目标类型上的值类型作为callback参数的类型。

// 定义一个工具类型,特殊处理定义一个 on 函数的类型,主要是修饰 on 的参数类型
type PropertiesEventChange<T> = {
  on<K extends string & keyof T >(eventName: `${K}Changed`, callback: (newValue:T[K]) => void):void
}


declare function makeWatchedObject<Type>(obj: Type): Type & PropertiesEventChange<Type>;


const p = makeWatchedObject({
  name: 'tony',
  age: 11
});


p.on('ageChanged', (age) => age);
// (method) on<"age">(eventName: "ageChanged", callback: (newValue: number) => void): void

模板字符串的解析原理

TypeScript 在编译过程中,编译器会将代码解析成 AST (抽象语法树)。对于模板字符串编译器会识别出模板字符串的静态部分(普通的字符文本)和动态部分(插值表达式)解析的结果仍然是AST结构,完成解析以后TypeScript 会进行类型推导。类型推导时编译器会分析每个插值表达式的类型,根据类型不同进行转换推导出整个模板字符串的类型。这里会有类似于 JavaScript 类型转换的场景非字符如何 toPrimitive 。

TS代码:

const name = 'Alice';
const age = 30;
const message = `Hello, my name is ${name} and I am ${age} years old.`;

解析成类似AST语法树:

TemplateLiteral
└── Quasi (Hello, my name is )
└── Expression (name)
└── Quasi ( and I am )
└── Expression (age)
└── Quasi ( years old.)

解析模板字符串的结构,然后根据插值表达式的类型推导出整个模板字符串的类型。

字符串操作工具类型【有的说是高级类型】

TypeScript 为了提升对字符串的操作能力(大小写转换、首字母大小写转换),内置封装了四个主要的工具类型。分别是 Uppercase<StringType>、Lowercase<StringType>、Capitalize<StringType>、Uncapitalize<StringType> 这些类型并不是像 Omit、Record 等高级类型在 type.d.ts 中自定义封装的,Typescript 为了性能将其内置在编译器中实现【源码实在没看明白,抱歉了诸位】。

我们来分别看看他们的功能:

Uppercase<StringType>

将字符串中的每个字母都转换成大写。

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

我们来实现这样一个简单的题目:将一个对象上的所有属性名提取出来,并转成大写。

// 限制一下K从T的属性中提取,并且是string类型
type PickPropertyToUppercase<T, K extends string & keyof T> = Uppercase<K>;

const per = {
  name: 'tony',
  age: 11,
}

type PerType = PickPropertyToUppercase<typeof per, keyof typeof per>
// type PerType = "NAME" | "AGE"


// 如果你想只有一个入口,也可以这样再封一个自定义的类型工具
type A<T extends { [k: string ]: any }> = PickPropertyToUppercase<T, keyof T>;
type APerType = A<typeof per>
//type APerType = "NAME" | "AGE"

这些基础能力结合我们上一篇文章中的类型基础【keyof、 typeof、infer、extends、条件类型~】就是我们玩转体操的基础啦~

Lowercase<StringType>

将字符串中所有的字母都变成小写,和 Uppercase 用法是类似的。

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

Capitalize<StringType>、Uncapitalize<StringType>

将字符中的首字母大写首字母小写大家一看也都懂,我们来写一个实现一个题目:下划线命名转小驼峰的命名方式。

比如: aa_bb_cc 转换成 aaBbCc 。

实现思路:利用条件类型进行检查,将字符串按照 _ 下划线进行分割,然后下划线前后部分进行首字母大写,最后再将结果进行首字母小写。

第一步:剔除下划线,利用条件类型提取 _ 下划线分割的前后两个部分,然后组合出新的字符串。

type RemoveBaseline<T extends string>T extends `${infer FirstStr}_${infer LastStr}` ? `${FirstStr}${LastStr}` : T;
type NotBaselineStr = RemoveBaseline<"aa_bb">
// type NotBaselineStr = "aabb"

第二步:在第一步的基础上将 FirstStr、LastStr 首字母大写进行组合。

type UpperCamel<T extends string> T extends `${infer FirstStr}_${infer LastStr}` ? `${Capitalize<FirstStr>}${Capitalize<LastStr>}` : T;

type Camelstr = UpperCamel<"aa_bb">
// type Camelstr = "AaBb"

第三步: 步骤二的结果只能实现第一个_下划线的转换,需要将字符串中所有的下划线进行转换需要加上递归处理【分别_分开的字符串进行左右递归】,将所有的 _ 下划线剔除。 最后将结果进行首字母大写,即可上菜。

type UpperCamel<T extends string> T extends `${infer FirstStr}_${infer LastStr}` ? `${Capitalize<UpperCamel<FirstStr>>}${Capitalize<UpperCamel<LastStr>>}` : T;

type Camelstr = UpperCamel<"aa_bb_cc___dd">
//type Camelstr = "AaBbCcDd"



// 再将 AaBbCcDd -> aaBbCcDd 这个方式就很简单了 直接调用一次 Uncapitalize 
type LowerCamel<T extends string> = Uncapitalize<UpperCamel<T>>;

type lowerCamelStr = LowerCamel<"aa_bb_cc___dd">
// type lowerCamelStr = "aaBbCcDd"

至此,字符串的类型操作以及模板字符串中的相关技术点就介绍完了。结合《TypeScript 玩转类型操作之基础篇》我们体操的能力已经具备,下一篇我们搞几个面试中经常被问到的TS体操操作,练练手。欢迎大家评论区推荐一下。

大佬~ 点个赞哇~ 给您磕头了

参考资料

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Refer...

https://www.typescriptlang.org/docs/handbook/2/template-liter...

https://juejin.cn/post/7257138433574060092


小乌龟快跑
15 声望2 粉丝