0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

一文详细了解类型体操

我快闭嘴 来源:OSC开源社区 作者:OSC开源社区 2022-09-27 10:07 次阅读

今天给大家分享的主题是一起来做类型体操。

主要分为 4 个部分进行介绍:

  1. 类型体操的背景,通过背景了解为什么要在项目中加入类型体操;
  2. 了解类型体操的主要类型、运算逻辑、和类型套路;
  3. 类型体操实践,解析 TypeScript 内置高级类型,手写 ParseQueryString 复杂类型;
  4. 小结,综上分享,沉淀结论。

一、背景

在背景章节介绍的是什么是类型,什么是类型安全,怎么实现类型安全,什么是类型体操?

以了解类型体操的意义。

1. 什么是类型?

了解什么是类型之前,先来介绍两个概念:

  • 不同类型变量占据的内存大小不同

boolean 类型的变量会分配 4 个字节的内存,而 number 类型的变量则会分配 8 个字节的内存,给变量声明了不同的类型就代表了会占据不同的内存空间。

  • 不同类型变量可做的操作不同

number 类型可以做加减乘除等运算,boolean 就不可以,复合类型中不同类型的对象可用的方法不同,比如 Date 和 RegExp,变量的类型不同代表可以对该变量做的操作就不同。

综上,可以得到一个简单的结论就是,类型就是编程语言提供对不同内容的抽象定义

2. 什么是类型安全?

了解了类型的概念后,那么,什么是类型安全呢?

一个简单的定义就是,类型安全就是只做该类型允许的操作。比如对于 boolean 类型,不允许加减乘除运算,只允许赋值 true、false。

当我们能做到类型安全时,可以大量的减少代码中潜在的问题,大量提高代码质量。

3. 怎么实现类型安全?

那么,怎么做到类型安全?

这里介绍两种类型检查机制,分别是动态类型检查和静态类型检查。

3.1 动态类型检查

Javascript 就是典型的动态类型检查,它在编译时,没有类型信息,到运行时才检查,导致很多隐藏 bug。

6e1169d8-3d9e-11ed-9e49-dac502259ad0.jpg

3.2 静态类型检查

TypeScript 作为 Javascript 的超集,采用的是静态类型检查,在编译时就有类型信息,检查类型问题,减少运行时的潜在问题。

6e1fce4c-3d9e-11ed-9e49-dac502259ad0.jpg

4. 什么是类型体操

上面介绍了类型的一些定义,都是大家熟悉的一些关于类型的背景介绍,这一章节回归到本次分享的主题概念,类型体操。

了解类型体操前,先介绍 3 种类型系统。

4.1 简单类型系统

简单类型系统,它只基于声明的类型做检查,比如一个加法函数,可以加整数也可以加小数,但在简单类型系统中,需要声明 2 个函数来做这件事情。

intadd(inta,intb){
returna+b
}

doubleadd(doublea,doubleb){
returna+b
}

4.2 泛型类型系统

泛型类型系统,它支持类型参数,通过给参数传参,可以动态定义类型,让类型更加灵活。

Tadd(Ta,Tb){
returna+b
}

add(1,2)
add(1.1,2.2)

但是在一些需要类型参数逻辑运算的场景就不适用了,比如一个返回对象某个属性值的函数类型。

functiongetPropValue<T>(obj:T,key){
returnobj[key]
}

4.3 类型编程系统

类型编程系统,它不仅支持类型参数,还能给类型参数做各种逻辑运算,比如上面提到的返回对象某个属性值的函数类型,可以通过 keyof、T[K] 来逻辑运算得到函数类型。

functiongetPropValue<
  Textendsobject,
KeyextendskeyofT
>(obj:T,key:Key):T[Key]{
returnobj[key]
}

总结上述,类型体操就是类型编程,对类型参数做各种逻辑运算,以产生新的类型

之所以称之为体操,是因为它的复杂度,右侧是一个解析参数的函数类型,里面用到了很多复杂的逻辑运算,等先介绍了类型编程的运算方法后,再来解析这个类型的实现。

二、了解类型体操

熟悉完类型体操的概念后,再来继续了解类型体操有哪些类型,支持哪些运算逻辑,有哪些运算套路。

1. 有哪些类型

类型体操的主要类型列举在图中。TypeScript 复用了 JS 的基础类型和复合类型,并新增元组(Tuple)、接口(Interface)、枚举(Enum)等类型,这些类型在日常开发过程中类型声明应该都很常用,不做赘述。

6e374388-3d9e-11ed-9e49-dac502259ad0.jpg
//元组(Tuple)就是元素个数和类型固定的数组类型
typeTuple=[number,string];

//接口(Interface)可以描述函数、对象、构造器的结构:
interfaceIPerson{
name:string;
age:number;
}

classPersonimplementsIPerson{
name:string;
age:number;
}

constobj:IPerson={
name:'aa',
age:18
}

//枚举(Enum)是一系列值的复合:
enumTranspiler{
Babel='babel',
Postcss='postcss',
Terser='terser',
Prettier='prettier',
TypeScriptCompiler='tsc'
}

consttranspiler=Transpiler.TypeScriptCompiler;

2. 运算逻辑

重点介绍的是类型编程支持的运算逻辑。

TypeScript 支持条件、推导、联合、交叉、对联合类型做映射等 9 种运算逻辑。

  • 条件:T extends U ? X : Y

条件判断和 js 逻辑相同,都是如果满足条件就返回 a 否则返回 b。

//条件:extends?:
//如果T是2的子类型,那么类型是true,否则类型是false。
typeisTwo=Textends2?true:false;
//false
typeres=isTwo<1>;
  • 约束:extends

通过约束语法 extends 限制类型。

//通过TextendsLength约束了T的类型,必须是包含length属性,且length的类型必须是number。
interfaceLength{
length:number
}

functionfn1<TextendsLength>(arg:T):number{
returnarg.length
}
  • 推导:infer

推导则是类似 js 的正则匹配,都满足公式条件时,可以提取公式中的变量,直接返回或者再次加工都可以。

//推导:infer
//提取元组类型的第一个元素:
//extends约束类型参数只能是数组类型,因为不知道数组元素的具体类型,所以用unknown。
//extends判断类型参数T是不是[inferF,...inferR]的子类型,如果是就返回F变量,如果不是就不返回
typeFirst=Textends[inferF,...inferR]?F:never;
//1
typeres2=First<[1,2,3]>;
  • 联合:|

联合代表可以是几个类型之一。

typeUnion=1|2|3
  • 交叉:&

交叉代表对类型做合并。

typeObjType={a:number}&{c:boolean}

keyof 用于获取某种类型的所有键,其返回值是联合类型。

//consta:'name'|'age'='name'
consta:keyof{
name:string,
age:number
}='name'
  • 索引访问:T[K]

T[K] 用于访问索引,得到索引对应的值的联合类型。

interfaceI3{
name:string,
age:number
}

typeT6=I3[keyofI3]//string|number

  • 索引遍历: in

in 用于遍历联合类型。

constobj={
name:'tj',
age:11
}

typeT5={
[Pinkeyoftypeofobj]:any
}

/*
{
name:any,
age:any
}
*/
  • 索引重映射: as

as 用于修改映射类型的 key。

//通过索引查询keyof,索引访问t[k],索引遍历in,索引重映射as,返回全新的key、value构成的新的映射类型
typeMapType={
[
KeyinkeyofT
as`${Key&string}${Key&string}${Key&string}`
]:[T[Key],T[Key],T[Key]]
}
//{
//aaa:[1,1,1];
//bbb:[2,2,2];
//}
typeres3=MapType<{ a:1,b:2}>

3. 运算套路

根据上面介绍的 9 种运算逻辑,我总结了 4 个类型套路。

  • 模式匹配做提取;
  • 重新构造做变换;
  • 递归复用做循环;
  • 数组长度做计数。

3.1 模式匹配做提取

第一个类型套路是模式匹配做提取。

模式匹配做提取的意思是通过类型 extends 一个模式类型,把需要提取的部分放到通过 infer 声明的局部变量里。

举个例子,用模式匹配提取函数参数类型。

typeGetParametersFunction>=
Funcextends(...args:inferArgs)=>unknown?Args:never;

typeParametersResult=GetParameters<(name:string,age:number)=>string>

首先用 extends 限制类型参数必须是 Function 类型。

然后用 extends 为 参数类型匹配公式,当满足公式时,提取公式中的变量 Args。

实现函数参数类型的提取。

3.2 重新构造做变换

第二个类型套路是重新构造做变换。

重新构造做变换的意思是想要变化就需要重新构造新的类型,并且可以在构造新类型的过程中对原类型做一些过滤和变换。

比如实现一个字符串类型的重新构造。

typeCapitalizeStr=
Strextends`${inferFirst}${inferRest}`
?`${Uppercase}${Rest}`:Str;

typeCapitalizeResult=CapitalizeStr<'tang'>

首先限制参数类型必须是字符串类型。

然后用 extends 为参数类型匹配公式,提取公式中的变量 First Rest,并通过 Uppercase 封装。

实现了首字母大写的字符串字面量类型。

3.3 递归复用做循环

第三个类型套路是递归复用做循环。

TypeScript 本身不支持循环,但是可以通过递归完成不确定数量的类型编程,达到循环的效果。

比如通过递归实现数组类型反转。

typeReverseArr=
Arrextends[inferFirst,...inferRest]
?[...ReverseArr,First]
:Arr;


typeReverseArrResult=ReverseArr<[1,2,3,4,5]>

首先限制参数必须是数组类型。

然后用 extends 匹配公式,如果满足条件,则调用自身,否则直接返回。

实现了一个数组反转类型。

3.4 数组长度做计数

第四个类型套路是数组长度做计数。

类型编程本身是不支持做加减乘除运算的,但是可以通过递归构造指定长度的数组,然后取数组长度的方式来完成数值的加减乘除。

比如通过数组长度实现类型编程的加法运算。

typeBuildArray<
    Length extends number,
    Ele = unknown,
    Arr extends unknown[] = []
    >=Arr['length']extendsLength
?Arr
:BuildArray;

typeAdd=
[...BuildArray,...BuildArray]['length'];


typeAddResult=Add<32,25>

首先通过递归创建一个可以生成任意长度的数组类型

然后创建一个加法类型,通过数组的长度来实现加法运算。

三、类型体操实践

分享的第三部分是类型体操实践。

前面分享了类型体操的概念及常用的运算逻辑。

下面我们就用这些运算逻辑来解析 TypeScript 内置的高级类型。

1. 解析 TypeScript 内置高级类型

  • partial 把索引变为可选

通过 in 操作符遍历索引,为所有索引添加 ?前缀实现把索引变为可选的新的映射类型。

typeTPartial={
[PinkeyofT]?:T[P];
};

typePartialRes=TPartial<{ name:'aa',age:18}>
  • Required 把索引变为必选

通过 in 操作符遍历索引,为所有索引删除 ?前缀实现把索引变为必选的新的映射类型。

typeTRequired={
[PinkeyofT]-?:T[P]
}

typeRequiredRes=TRequired<{ name?: 'aa',age?:18}>
  • Readonly 把索引变为只读

通过 in 操作符遍历索引,为所有索引添加 readonly 前缀实现把索引变为只读的新的映射类型。

typeTReadonly={
readonly[PinkeyofT]:T[P]
}

typeReadonlyRes=TReadonly<{ name?: 'aa',age?:18}>
  • Pick 保留过滤索引

首先限制第二个参数必须是对象的 key 值,然后通过 in 操作符遍历第二个参数,生成新的映射类型实现。

typeTPick={
[PinK]:T[P]
}

typePickRes=TPick<{ name?: 'aa',age?:18},'name'>
  • Record 创建映射类型

通过 in 操作符遍历联合类型 K,创建新的映射类型。

typeTRecord={
[PinK]:T
}

typeRecordRes=TRecord<'aa'|'bb',string>
  • Exclude 删除联合类型的一部分

通过 extends 操作符,判断参数 1 能否赋值给参数 2,如果可以则返回 never,以此删除联合类型的一部分。

typeTExclude=TextendsU?never:T

typeExcludeRes=TExclude<'aa'|'bb','aa'>
  • Extract 保留联合类型的一部分

和 Exclude 逻辑相反,判断参数 1 能否赋值给参数 2,如果不可以则返回 never,以此保留联合类型的一部分。

typeTExtract=TextendsU?T:never

typeExtractRes=TExtract<'aa'|'bb','aa'>
  • Omit 删除过滤索引

通过高级类型 Pick、Exclude 组合,删除过滤索引。

typeTOmit=Pick>

typeOmitRes=TOmit<{ name:'aa',age:18},'name'>
  • Awaited 用于获取 Promise 的 valueType

通过递归来获取未知层级的 Promise 的 value 类型。

typeTAwaited=
Textendsnull|undefined
?T
:Textendsobject&{then(onfulfilled:inferF):any}
?Fextends((value:inferV,...args:any)=>any)
?Awaited
:never
:T;


typeAwaitedRes=TAwaited<Promise<Promise<Promise>>>

还有非常多高级类型,实现思路和上面介绍的类型套路大多一致,这里不一一赘述。

2. 解析 ParseQueryString 复杂类型

重点解析的是在背景章节介绍类型体操复杂度,举例说明的解析字符串参数的函数类型。

如图示 demo 所示,这个函数是用于将指定字符串格式解析为对象格式。

functionparseQueryString1(queryStr){
if(!queryStr||!queryStr.length){
return{}
}
constqueryObj={}
constitems=queryStr.split('&')
items.forEach((item)=>{
const[key,value]=item.split('=')
if(queryObj[key]){
if(Array.isArray(queryObj[key])){
queryObj[key].push(value)
}else{
queryObj[key]=[queryObj[key],value]
}
}else{
queryObj[key]=value
}
})
returnqueryObj
}

比如获取字符串 a=1&b=2 中 a 的值。

常用的类型声明方式如下图所示:

functionparseQueryString1(queryStr:string):Record<string,any>{
if(!queryStr||!queryStr.length){
return{}
}
constqueryObj={}
constitems=queryStr.split('&')
items.forEach((item)=>{
const[key,value]=item.split('=')
if(queryObj[key]){
if(Array.isArray(queryObj[key])){
queryObj[key].push(value)
}else{
queryObj[key]=[queryObj[key],value]
}
}else{
queryObj[key]=value
}
})
returnqueryObj
}

参数类型为 string,返回类型为 Record,这时看到,res1.a 类型为 any,那么有没有办法,准确的知道 a 的类型是字面量类型 1 呢?

下面就通过类型体操的方式,来重写解析字符串参数的函数类型。

首先限制参数类型是 string 类型,然后为参数匹配公式 a&b,如果满足公式,将 a 解析为 key value 的映射类型,将 b 递归 ParseQueryString 类型,继续解析,直到不再满足 a&b 公式。

最后,就可以得到一个精准的函数返回类型,res.a = 1

typeParseParam=
Paramextends`${inferKey}=${inferValue}`
?{
[KinKey]:Value
}:Record;

typeMergeParams<
    OneParam extends Record,
OtherParamextendsRecord
>={
readonly[KeyinkeyofOneParam|keyofOtherParam]:
KeyextendskeyofOneParam
?OneParam[Key]
:KeyextendskeyofOtherParam
?OtherParam[Key]
:never
}

typeParseQueryString=
Strextends`${inferParam}&${inferRest}`
?MergeParams,ParseQueryString>
:ParseParam;
functionparseQueryString<Strextendsstring>(queryStr:Str):ParseQueryString<Str>{
if(!queryStr||!queryStr.length){
return{}asany;
}
constqueryObj={}asany;
constitems=queryStr.split('&');
items.forEach(item=>{
const[key,value]=item.split('=');
if(queryObj[key]){
if(Array.isArray(queryObj[key])){
queryObj[key].push(value);
}else{
queryObj[key]=[queryObj[key],value]
}
}else{
queryObj[key]=value;
}
});
returnqueryObjasany;
}


constres=parseQueryString('a=1&b=2&c=3');

console.log(res.a)//type1

四、小结

综上分享,从 3 个方面介绍了类型体操。

  • 第一点是类型体操背景,了解了什么是类型,什么是类型安全,怎么实现类型安全;

  • 第二点是熟悉类型体操的主要类型、支持的逻辑运算,并总结了 4 个类型套路;

  • 第三点是类型体操实践,解析了 TypeScript 内置高级类型的实现,并手写了一些复杂函数类型。

从中我们了解到需要动态生成类型的场景,必然是要用类型编程做一些运算,即使有的场景下可以不用类型编程,但是使用类型编程能够有更精准的类型提示和检查,减少代码中潜在的问题。

审核编辑:汤梓红


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 内存
    +关注

    关注

    8

    文章

    3019

    浏览量

    74003
  • 编程
    +关注

    关注

    88

    文章

    3614

    浏览量

    93686

原文标题:类型体操的9种类型运算、4种类型套路总结

文章出处:【微信号:OSC开源社区,微信公众号:OSC开源社区】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    详细了解JTAG接口

    在FPGA研发及学习过程中,有个关键步骤就是下板实现,做硬件“硬现”很重要,般来说用JTAG口比较常见些,因此相信肯定有些大侠遇到过JTAG口失灵或者损坏无法使用的事情。最近我就遇到了这类事情
    发表于 07-20 09:15 1.2w次阅读

    了解MyBatis的查询原理

    可以详细了解MyBatis的次查询过程。在平时的代码编写中,发现了MyBatis个低版本的bug(3.4.5之前的版本),由于现在很多工程中的版本都是低于3.4.5的,因此在这里用
    的头像 发表于 10-10 11:42 1422次阅读

    用VDK+BF537开发产品中,想详细了解下VDK中事件、事件bit、信号量的使用方法,以及如何写自己的device drivers

    用VDK+BF537开发产品中,想详细了解下VDK中事件、事件bit、信号量的使用方法,以及如何写自己的device drivers。请问哪有针对上述问题的资料下载?
    发表于 12-06 09:19

    详细了解下ups的相关计算

    关于ups方面的计算有很多,ups无论是接空开,还是连接电缆,以及选择电池,都可能需要计算它的电流或功率等,那么今天我们来详细了解下ups的相关计算。、UPS电源及电流、高频ups与工频ups
    发表于 11-16 09:08

    详细了解下STM32F1的具体电路参数

    最近笔者在使用STM32时,需要详细了解下F1的具体电路参数。于是查看其官方数据手册,结果记录如下。绝对最大额度值般工作条件表中的FT指5V 耐压。可以在引脚定义表格中看到。I/O端口特性(逻辑电平)在最后
    发表于 01-18 07:07

    带你详细了解HarmonyOS折叠屏设计规范!

    /docs/design/des-guides/basic-requirements-0000001193421226),了解更多HarmonyOS折叠屏设计规范的详细内容。
    发表于 05-20 10:22

    通过 iftop、 nethogs 和 vnstat 详细了解你的网络连接状态

    通过 iftop、 nethogs 和 vnstat 详细了解你的网络连接状态。
    的头像 发表于 01-27 21:10 2.1w次阅读
    通过 iftop、 nethogs 和 vnstat <b class='flag-5'>详细了解</b>你的网络连接状态

    详细了解HarmonyOS工程

    华为云享专家,InfoQ签约作者,阿里云专家博主,51CTO博客首席体验官,开源项目GVA成员之,专注于大前端技术的分享,包括Flutter,小程序,安卓,VUE,JavaScript。
    的头像 发表于 02-28 10:53 1406次阅读
    <b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>详细了解</b>HarmonyOS工程

    详细了解Cgroup

    cgroup最基本的操作时我们可以使用以下命令创建个cgroup文件夹
    的头像 发表于 04-20 11:10 5860次阅读

    详细了解OpenHarmony新图形框架

    3月30日,OpenHarmony v3.1 Release版本正式发布了。此版本为大家带来了全新的图形框架,实现了UI框架显示、多窗口、流畅动画等基础能力,夯实了OpenHarmony系统能力基座。下面就带大家详细了解新图形框架。
    的头像 发表于 04-27 13:21 2208次阅读
    <b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>详细了解</b>OpenHarmony新图形框架

    详细了解HTTP协议

    Http协议即超文本传送协议 (HTTP-Hypertext transfer protocol) 。
    的头像 发表于 05-11 12:04 1698次阅读

    详细了解交换机数据接口类型

    交换机的接口类型变得非常丰富,为了让大家对这些接口有个比较清晰的认识今天我们就聊下交换机数据接口类型,方便大家选择。
    发表于 06-21 14:51 2635次阅读

    详细了解CCIX规范

    正文开始前,闲扯几句。在接下来分析CCIX规范的过程中,大家会发现CCIX里面有太多ARM的影子,尤其是协议层的致性协议部分,你会看到有很多跟CHI相似的东西。另外,在CCIX规范的底层,基本全是复用和遵循PCIe规范。
    的头像 发表于 06-23 09:20 2021次阅读

    带您详细了解IEEE802.3bt(PoE++)的有关特点

    Hqst华强盛(盈盛电子)导读:带您详细了解IEEE802.3bt(PoE++)的有关特点,让我们对IEEE802.3bt(PoE++)协议有更具体的了解
    的头像 发表于 01-04 11:26 2158次阅读
    带您<b class='flag-5'>一</b>起<b class='flag-5'>详细了解</b>IEEE802.3bt(PoE++)的有关特点

    带你详细了解工业电脑

    扇设计、承受振动和恶劣环境的能力、轻松配置、全面的I/O选项、延长生命周期、耐用的组件。了解如何为您的应用选择工业电脑对提高设施的生产力和效率至关重要。详细了解
    的头像 发表于 06-12 14:24 401次阅读
    <b class='flag-5'>一</b><b class='flag-5'>文</b>带你<b class='flag-5'>详细了解</b>工业电脑