TypeScript 玩转类型操作之字符串处理能力

引言

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

模板字符串

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

typescript 复制代码
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 中模板字符串的相关特性

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

构造新的类型

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

typescript 复制代码
type World = `world`;
type Greeting = `hello ${World}`;
// type Greeting = "hello world";

在插值位置使用联合类型时,构造出一个新的字符串联合类型。

typescript 复制代码
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代码如下:

typescript 复制代码
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 函数的类型,定义其返回值类型为传入的值类型。

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

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

typescript 复制代码
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参数的类型。

typescript 复制代码
// 定义一个工具类型,特殊处理定义一个 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代码:

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

解析成类似AST语法树:

typescript 复制代码
TemplateLiteral
└── Quasi (Hello, my name is )
└── Expression (name)
└── Quasi ( and I am )
└── Expression (age)
└── Quasi ( years old.)

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

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

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

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

Uppercase

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

typescript 复制代码
type Greeting = "Hello, world"
type ShoutyGreeting = Uppercase<Greeting>
//type ShoutyGreeting = "HELLO, WORLD"

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

typescript 复制代码
// 限制一下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

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

typescript 复制代码
type Greeting = "Hello, world"
type QuietGreeting = Lowercase<Greeting>
// type QuietGreeting = "hello, world"

Capitalize、Uncapitalize

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

比如: aa_bb_cc 转换成 aaBbCc 。

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

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

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

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

typescript 复制代码
type UpperCamel<T extends string> T extends `${infer FirstStr}_${infer LastStr}` ? `${Capitalize<FirstStr>}${Capitalize<LastStr>}` : T;

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

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

typescript 复制代码
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体操操作,练练手。欢迎大家评论区推荐一下。

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

参考资料

developer.mozilla.org/zh-CN/docs/...

www.typescriptlang.org/docs/handbo...

juejin.cn/post/725713...

相关推荐
前端之虎陈随易35 分钟前
2年没用Nodejs了,Bun很香
linux·前端·javascript·vue.js·typescript
Yue1681 小时前
啥子都能看懂的TypeScript快速入门
typescript
好运的阿财2 小时前
OpenClaw工具拆解之host_workspace_write+host_workspace_edit
前端·javascript·人工智能·机器学习·ai编程·openclaw·openclaw工具
XiYang-DING2 小时前
JavaScript
开发语言·javascript·ecmascript
空中海3 小时前
02 React Native状态、导航、数据流与设备能力
javascript·react native·react.js
空中海4 小时前
02 状态、Hooks、副作用与数据流
开发语言·javascript·ecmascript
空中海4 小时前
04 React Native工程化、质量、发布与生态选型
javascript·react native·react.js
李白的天不白4 小时前
webpack 与axios 版本冲突问题
前端·webpack·node.js
杨超凡5 小时前
豆包收费了?我特么自己用“意念”搓了一个!
javascript