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

发表于 3年以前  | 总阅读数:415 次

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

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

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

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

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

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

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

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

我们会做这些体操:

  • 用 ts 类型实现加法
  • 用 ts 类型生成重复 N 次的字符串
  • 用 ts 类型实现简易的 js parser(部分)
  • 用 ts 类型实现对象属性按条件过滤

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

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 属性,那不就是个数字么。可以通过构造一定长度的数组来实现加法。

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

type createArray<Len, Ele, Arr extends Ele[] = []> =  Arr['length'] extends Len ? Arr : createArray<Len, Ele, [Ele, ...Arr]>

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

type Add<A extends number, B extends number> = [...createArray<A, 1>, ...createArray<B, 1>]['length']

我们测试下:

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

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

字符串类的体操

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

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

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

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

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

type RepeactStr<Str extends string,
                Count, 
                Arr extends Str[] = [],
                ResStr extends string = ''> 
 = Arr['length'] extends Count 
 ? ResStr 
 : RepeactStr<Str,Count, [Str, ...Arr], `${Str}${ResStr}`>;

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

测试一下:

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

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

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

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

解析函数名

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

我们先定义字母的类型:

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

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

type TempParseResult<Token extends string, Rest extends string> = {
    token: Token,
    rest: Rest
}

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

type parseFunctionName<SourceStr extends string, Res extends string = ''> 
  = SourceStr extends `${infer PrefixChar}${infer RestStr}` 
    ?  PrefixChar extends alphaChars 
    ?  parseFunctionName<RestStr, `${Res}${PrefixChar}`> 
    : TempParseResult<Res, SourceStr> 
    : never;

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

测试一下:

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

然后继续解析剩下的。

解析括号

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

type brackets = '(' | ')';
type parseBrackets<SourceStr> 
    = SourceStr extends `${infer PrefixChar}${infer RestStr}` 
    ?  PrefixChar extends brackets 
    ?  TempParseResult<PrefixChar, RestStr> 
    : never 
    : never;

测试一下:

继续解析剩下的:

解析数字

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

type numChars = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';

type parseNum<SourceStr extends string, Res extends string = ''> 
    = SourceStr extends `${infer PrefixChar}${infer RestStr}` 
    ?  PrefixChar extends numChars 
    ? parseNum<RestStr, `${Res}${PrefixChar}`> 
    : TempParseResult<Res, SourceStr> 
    : never;

测试一下:

继续解析剩下的:

解析逗号

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

type parseComma<SourceStr extends string> 
    = SourceStr extends `${infer PrefixChar}${infer RestStr}` 
    ?  PrefixChar extends ',' 
    ?  TempParseResult<',', RestStr> 
    : never 
    : never;

测试一下:

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

整体解析

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

type parse<SourceStr extends string, Res extends string = ''> 
    = parseFunctionName<SourceStr, Res> extends TempParseResult<infer FunctionName, infer Rest1> 
    ? parseBrackets<Rest1> extends TempParseResult<infer BracketChar, infer Rest2> 
    ? parseNum<Rest2> extends TempParseResult<infer Num1, infer Rest3> 
    ? parseComma<Rest3>  extends TempParseResult<infer CommaChar, infer Rest4> 
    ? parseNum<Rest4>  extends TempParseResult<infer Num2, infer Rest5> 
    ? parseBrackets<Rest5>  extends TempParseResult<infer BracketChar2, infer Rest6> 
    ? {
        functionName: FunctionName,
        params: [Num1, Num2],
    }: never: never: never: never : never : never;

测试一下:

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

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

完整代码如下:

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

type TempParseResult<Token extends string, Rest extends string> = {
    token: Token,
    rest: Rest
}

type parseFunctionName<SourceStr extends string, Res extends string = ''> = 
    SourceStr extends `${infer PrefixChar}${infer RestStr}` 
    ?  PrefixChar extends alphaChars 
    ?  parseFunctionName<RestStr, `${Res}${PrefixChar}`> 
    : TempParseResult<Res, SourceStr> 
    : never;

type brackets = '(' | ')';
type parseBrackets<SourceStr> 
    = SourceStr extends `${infer PrefixChar}${infer RestStr}` 
    ?  PrefixChar extends brackets 
    ?  TempParseResult<PrefixChar, RestStr> 
    : never 
    : never;

type parseNum<SourceStr extends string, Res extends string = ''> 
    = SourceStr extends `${infer PrefixChar}${infer RestStr}` 
    ?  PrefixChar extends numChars 
    ? parseNum<RestStr, `${Res}${PrefixChar}`> 
    : TempParseResult<Res, SourceStr> 
    : never;

type parseComma<SourceStr extends string> 
    = SourceStr extends `${infer PrefixChar}${infer RestStr}` 
    ?  PrefixChar extends ',' 
    ?  TempParseResult<',', RestStr> 
    : never 
    : never;

type parse<SourceStr extends string, Res extends string = ''> 
    = parseFunctionName<SourceStr, Res> extends TempParseResult<infer FunctionName, infer Rest1> 
    ? parseBrackets<Rest1> extends TempParseResult<infer BracketChar, infer Rest2> 
    ? parseNum<Rest2> extends TempParseResult<infer Num1, infer Rest3> 
    ? parseComma<Rest3>  extends TempParseResult<infer CommaChar, infer Rest4> 
    ? parseNum<Rest4>  extends TempParseResult<infer Num2, infer Rest5> 
    ? parseBrackets<Rest5>  extends TempParseResult<infer BracketChar2, infer Rest6> 
    ? {
        functionName: FunctionName,
        params: [Num1, Num2],
    }: never: never: never: never : never : never;

type res = parse<'add(11,2)'>;

对象类的体操

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

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

type filterNumberProp<T extends Object> = { 
    [Key in keyof T] : T[Key] extends number ? T[Key] : never
 }[keyof T];

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

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

测试一下:

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

总结

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

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

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

  • 有 ? : 可以做条件判断,常配合 extends 使用
  • 通过递归可以实现循环
  • 可以做对象的构造 {}、取属性名 keyof、取属性值 T[Key]
  • 可以做字符串的构造 ${a}${b},字符串的模式匹配来取子串 str extends ${infer x}${infer y}

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

  • ts 实现加法:通过递归构造数组再取长度
  • ts 实现重复字符串:递归构造数组来计数,然后递归构造字符串
  • ts 实现 parser:通过字符串模式匹配取子串的方式来解析每一部分,最后组合调用
  • ts 实现对象属性过滤:通过构造对象、取属性名、取值的语法组合调用

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

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

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

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/4ABCbFqtuOLxazkOc2-iwQ

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237276次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8111次阅读
 目录