文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

来做操吧!深入 TypeScript 高级类型和类型体操

2024-12-02 13:35

关注

TypeScript 给 JavaScript 扩展了类型的语法,我们可以给变量加上类型,在编译期间会做类型检查,配合编辑器还能做更准确的智能提示。此外,TypeScript 还支持了高级类型用于增加类型系统的灵活性。

就像 JavaScript 的高阶函数是生成函数的函数,React 的高阶组件是生成组件的组件一样,Typescript 的高级类型就是生成类型的类型。

TypeScript 高级类型是通过 type 定义的有类型参数(也叫泛型)的类型,它会对传入的类型参数做一系列的类型计算,产生新的类型。

  1. type Pick = { 
  2.     [P in K]: T[P]; 
  3. }; 

比如,这个 Pick 就是一个高级类型,它有类型参数 T 和 K,类型参数经过一系列的类型计算逻辑,会返回新的类型。

TypeScript 高级类型会根据类型参数求出新的类型,这个过程会涉及一系列的类型计算逻辑,这些类型计算逻辑就叫做类型体操。当然,这并不是一个正式的概念,只是社区的戏称,因为有的类型计算逻辑是比较复杂的。

TypeScript 的类型系统是图灵完备的,也就是说它能描述任何可计算逻辑,简单点来说就是循环、条件判断等该有的语法都有。

既然 TypeScript 的类型系统这么强,那我们就做一些高级类型的类型体操来感受下吧。

我们会做这些体操:

我把这些体操分为数字类的、字符串类的、对象类的,把这三种类型计算逻辑的规律掌握了,相信你的体操水平会提升一截。

TypeScript 类型语法基础

在做体操之前,要先过一下 TypeScript 的类型语法,也就是能做哪些类型计算逻辑。

既然说该有的语法都有,那我们来看下循环和判断都怎么做:

ts 类型的条件判断

ts 类型的条件判断的语法是 条件 ? 分支1 : 分支2 。

extends 关键字是用于判断 A 是否是 B 类型的。例子中传入的类型参数 T 是 1,是 number 类型,所以最终返回的是 true。

ts 类型的循环

ts 类型没有循环,但可以用递归来实现循环。

我们要构造一个长度为 n 的数组,那么就要传入长度的类型参数 Len、元素的类型参数 Ele、以及构造出的数组的类型参数 Arr(用于递归)。

然后类型计算逻辑就是判断 Arr 的 length 是否是 Len,如果是的话,就返回构造出的 Arr,不是的话就往其中添加一个元素继续构造。

这样,我们就递归的创建了一个长度为 Len 的数组。

ts 类型的字符串操作

ts 支持构造新的字符串: 

也支持根据模式匹配来取字符串中的某一部分:

因为 str 符合 aaa, 的模式,所以能够匹配上,把右边的部分放入通过 infer 声明的局部类型变量里,之后取该局部变量的值返回。

ts 类型的对象操作ts 支持对对象取属性、取值:


也可以创建新的对象类型:

通过 keyof 取出 obj 的所有属性名,通过 in 遍历属性名并取对应的属性值,通过这些来生成新的对象类型 newObj。

我们过了一下常用的 ts 类型的语法,包括条件判断、循环(用递归实现)、字符串操作(构造字符串、取某部分子串)、对象操作(构造对象、取属性值)。接下来就用这些来做操吧。

ts 类型体操练习

我们把体操分为 3 类来练习,之后再分别总结规律。

数字类的类型体操

体操 1:实现高级类型 Add,能够做数字加法。

ts 类型能做数字加法么?肯定可以的,因为它是图灵完备的,也就是各种可计算逻辑都可以做。

那怎么做呢?

数组类型可以取 length 属性,那不就是个数字么。可以通过构造一定长度的数组来实现加法。

上文我们实现了通过递归的方式实现了构造一定长度的新数组的高级类型:

  1. type createArray =  Arr['length'] extends Len ? Arr : createArray 

那只要分别构造两个不同长度的数组,然后合并到一起,再取 length 就行了。

  1. type Add = [...createArray, ...createArray]['length'

我们测试下:

我们通过构造数组的方式实现了加法!

小结下:ts 的高级类型想做数字的运算只能用构造不同长度的数组再取 length 的方式,因为没有类型的加减乘除运算符。

字符串类的体操体

体操2:把字符串重复 n 次。

字符串的构造我们前面学过了,就是通过 ${A}${B} 的方式,那只要坐下计数,判断下重复次数就行了。

计数涉及到了数字运算,要通过构造数组再取 length 的方式。

所以,我们要递归的构造数组来计数,并且递归的构造字符串,然后判断数组长度达到目标就返回构造的字符串。

所以有 Str(待重复的字符串)、Count(重复次数)、Arr(用于计数的数组)、ResStr(构造出的字符串)四个类型参数:

  1. type RepeactStr
  2.                 Count,  
  3.                 Arr extends Str[] = [], 
  4.                 ResStr extends string = ''>  
  5.  = Arr['length'] extends Count  
  6.  ? ResStr  
  7.  : RepeactStrCount, [Str, ...Arr], `${Str}${ResStr}`>; 

我们递归的构造了数组和字符串,判断构造的数组的 length 如果到了 Count,就返回构造的字符串 ResStr,否则继续递归构造。

测试一下:


小结:递归构造字符串的时候要通过递归构造数组来做计数,直到计数满足条件,就生成了目标的字符串。

这个体操只用到了构造字符串,没用到字符串通过模式匹配取子串,我们再做一个体操。

体操3: 实现简易的 JS Parser,能解析字符串 add(11,22) 的函数名和参数

字符串的解析需要根据模式匹配取子串。这里要分别解析函数名(functionName)、括号(brackets)、数字(num)、逗号(comma),我们分别实现相应的高级类型。

解析函数名

函数名是由字母构成,我们只要一个个字符一个字符的取,判断是否为字母,是的话就记录下该字符,然后对剩下的字符串递归进行同样的处理,直到不为字母的字符,通过这样的方式就能取出函数名。

我们先定义字母的类型:

  1. type alphaChars = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' 
  2.     | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' 
  3.     | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' 
  4.     | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z'

还有保存中间结果的类型:

  1. type TempParseResult = { 
  2.     token: Token, 
  3.     rest: Rest 

然后就一个个取字符来判断,把取到的字符构造成字符串存入中间结果:

  1. type parseFunctionName''>  
  2.   = SourceStr extends `${infer PrefixChar}${infer RestStr}`  
  3.     ?  PrefixChar extends alphaChars  
  4.     ?  parseFunctionName  
  5.     : TempParseResult  
  6.     : never; 

我们取了单个字符,然后判断是否是字母,是的话就把取到的字符构造成新的字符串,然后继续递归取剩余的字符串。

测试一下:

符合我们的需求,我们通过模式匹配取子串的方式解析出了函数名。

然后继续解析剩下的。

解析括号

括号的匹配也是同样的方式,而且括号只有一个字符,不需要递归的取,取一次就行。

  1. type brackets = '(' | ')'
  2. type parseBrackets  
  3.     = SourceStr extends `${infer PrefixChar}${infer RestStr}`  
  4.     ?  PrefixChar extends brackets  
  5.     ?  TempParseResult  
  6.     : never  
  7.     : never; 

测试一下:

继续解析剩下的:

解析数字

数字的解析也是一个字符一个字符的取,判断是否匹配,匹配的话就递归取下一个字符,直到不匹配:

  1. type numChars = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
  2.  
  3. type parseNum''>  
  4.     = SourceStr extends `${infer PrefixChar}${infer RestStr}`  
  5.     ?  PrefixChar extends numChars  
  6.     ? parseNum  
  7.     : TempParseResult  
  8.     : never; 

测试一下:

继续解析剩下的:

解析逗号

逗号和括号一样,只需要取一个字符判断即可,不需要递归。

  1. type parseComma  
  2.     = SourceStr extends `${infer PrefixChar}${infer RestStr}`  
  3.     ?  PrefixChar extends ','  
  4.     ?  TempParseResult<',', RestStr>  
  5.     : never  
  6.     : never; 

测试一下:

至此,我们完成了所有的字符的解析,解析来按照顺序组织起来就行。

整体解析

单个 token 的解析都做完了,整体解析就是组织下顺序,每次解析完拿到剩余的字符串传入下一个解析逻辑,全部解析完,就可以拿到各种信息。

  1. type parse''>  
  2.     = parseFunctionName extends TempParseResult  
  3.     ? parseBrackets extends TempParseResult  
  4.     ? parseNum extends TempParseResult  
  5.     ? parseComma  extends TempParseResult  
  6.     ? parseNum  extends TempParseResult  
  7.     ? parseBrackets  extends TempParseResult  
  8.     ? { 
  9.         functionName: FunctionName, 
  10.         params: [Num1, Num2], 
  11.     }: never: never: never: never : never : never; 

测试一下:

大功告成,我们用 ts 类型实现了简易的 parser!

小结:ts 类型可以通过模式匹配的方式取出子串,我们通过一个字符一个字符的取然后判断的方式,递归的拆分出 token,然后按照顺序拆分出 token,就能实现字符串的解析。

完整代码如下:

  1. type numChars = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
  2. type alphaChars = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' 
  3.     | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' 
  4.     | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' 
  5.     | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z'
  6.  
  7. type TempParseResult = { 
  8.     token: Token, 
  9.     rest: Rest 
  10.  
  11. type parseFunctionName''> =  
  12.     SourceStr extends `${infer PrefixChar}${infer RestStr}`  
  13.     ?  PrefixChar extends alphaChars  
  14.     ?  parseFunctionName  
  15.     : TempParseResult  
  16.     : never; 
  17.  
  18. type brackets = '(' | ')'
  19. type parseBrackets  
  20.     = SourceStr extends `${infer PrefixChar}${infer RestStr}`  
  21.     ?  PrefixChar extends brackets  
  22.     ?  TempParseResult  
  23.     : never  
  24.     : never; 
  25.  
  26. type parseNum''>  
  27.     = SourceStr extends `${infer PrefixChar}${infer RestStr}`  
  28.     ?  PrefixChar extends numChars  
  29.     ? parseNum  
  30.     : TempParseResult  
  31.     : never; 
  32.  
  33. type parseComma  
  34.     = SourceStr extends `${infer PrefixChar}${infer RestStr}`  
  35.     ?  PrefixChar extends ','  
  36.     ?  TempParseResult<',', RestStr>  
  37.     : never  
  38.     : never; 
  39.  
  40. type parse''>  
  41.     = parseFunctionName extends TempParseResult  
  42.     ? parseBrackets extends TempParseResult  
  43.     ? parseNum extends TempParseResult  
  44.     ? parseComma  extends TempParseResult  
  45.     ? parseNum  extends TempParseResult  
  46.     ? parseBrackets  extends TempParseResult  
  47.     ? { 
  48.         functionName: FunctionName, 
  49.         params: [Num1, Num2], 
  50.     }: never: never: never: never : never : never; 
  51.      
  52. type res = parse<'add(11,2)'>; 

对象类的体操

体操4:实现高级类型,取出对象类型中的数字属性值

构造对象、取属性名、取属性值的语法上文学过了,这里组合下就行:

  1. type filterNumberProp = {  
  2.     [Key in keyof T] : T[Key] extends number ? T[Key] : never 
  3.  }[keyof T]; 

我们构造一个新的对象类型,通过 keyof 遍历对象的属性名,然后对属性值做判断,如果不是数字就返回 never,然后再取属性值。

属性值返回 never 就代表这个属性不存在,就能达到过滤的效果。

测试一下:

小结:对象类型可以通过 {} 构造新对象,通过 [] 取属性值,通过 keyof 遍历属性名,综合这些语法就可以实现各种对象类型的逻辑。

总结

TypeScript 给 JavaScript 扩展了类型的语法,而且还支持了高级类型来生成类型。

高级类型是通过 type 声明的带有类型参数的类型,类型参数也叫泛型。根据类型参数生成最终类型的类型计算逻辑被戏称为类型体操。

TypeScript 的类型系统是图灵完备的,可以描述任何可计算逻辑:

我们分别做了这些类型体操:

其中要注意的就是数字类的要通过构造数组取长度的方式来计算,再就是字符串的模式匹配取子串,这两个是相对难度大一些的。

其实各种高级类型,只要熟悉了 ts 类型语法,想清楚了逻辑就能一步步写出来,和写 JS 逻辑没啥本质区别,只不过它是用于生成类型的逻辑。

读到这里,是不是感觉高级类型的类型体操也没有啥难度了呢?

【编辑推荐】

  1. 鸿蒙官方战略合作共建——HarmonyOS技术社区
  2. 如何在 Microsoft SQL Server 上恢复 SA 密码
  3. Linux 安装 MySQL 详细教程
  4. 微软 Windows 11/Windows 10 Office 全新 UI 已向所有用户推出:自适应亮/暗主题,简洁工具栏
  5. 盘点Python中四种读取Json文件和提取Json文件内容的方法
  6. Windows 11时代再难有用武之地?聊聊那些没落的软件
来源:神光的编程秘籍内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯