title: “TS 在项目中的 N 个实用小技巧 - 文字稿” date: 2022-03-10 tags: [“总结”, “分享”, “typescript”]

categories: [“项目经验”]

2 月底在早早聊 TS 专场相关的的分享,勤劳的把文字稿整理出来了来个首发。全文长度 1w+,内容较干,可以收藏起来慢慢看,或者结合视频一起看。希望对小伙伴们有帮助。

〇、前言

Hello 大家好。我叫鳗鱼,这次分享的专题是 ts 小技巧相关。之前已经分享过两次,可能有部分小伙伴已经认识了。我17 年毕业就在百度,现在 base 在深圳,技术上喜欢折腾各种新东西,爱好工程化相关。生活上有两只猫,喜欢出门找吃的和到处玩。

20220326232540
20220326232540

然后有一些惯例的前言声明:

  1. 针对用户:TypeScript 刚入门的新手。假设读者有基本的 TypeScript 常识,如果你对于 TypeScript 本身完全不了解,建议先阅读下文档。资深体操用户可选择绕行。
  2. 核心内容:项目经验总结而来的各种 tips 和 demo。代码较多,可能内容较干,自备矿泉水饮料
  3. 仅代表个人当前的经验,如有任何不足,随时交流

本次分享的目录主要分为四个部分:基础介绍常见技巧泛型介绍,和最后的其他&总结

一、基础介绍

我们先看一个漫画:

20220326232648
20220326232648

在 js 里面我们知道,0=='0'ture0==[]也会 true。但是呢,js 可以按照如果 a=b & a=c ,推导出 b=c 的逻辑吗?有经验的小伙伴肯定知道当然是不行,字符串 0 并不等于 []。 因此,上述漫画也成了社区里面经常讽刺 js 的一个梗,因为是弱类型语言,所以有着奇怪的隐式类型转换逻辑。

那么对应而言,ts 就有很多可以解决这些问题的能力了?不用讲应该多少都听说过,比如丰富的类型系统,强大的类型推导能力,完善的生态支持等等,这里就不做过多的概述。

20220326232707
20220326232707

1.1 基础语法复习

20220326232719
20220326232719

首先,对可能没怎么接触过的同学,复习下基础使用语法。在变量之后使用:进行类型标识,果没写他就会尝试推导。比如上图的,第一行 loadingboolean 类型,第二行 age 是数字类型。另外还有枚举,数组,接口,函数等类型,不熟悉的可以翻一下文档,这里仅展示示例。

一句话总结:核心语法就是冒号:,如果可选,则在冒号后面加个问号?:,比如右下图的 value。

1.2 注释的用处

另外,在写 ts 的时候,我们知道类型会被自动推断。像是函数的参数不需要再用 jsdoc 来写一遍。 不过如果想要使用的时候能有更好的提示,可以使用块级注释的形式,这样 ts 对于类型和描述都能一同体现在引用的地方的 doc 中,具体写法可以见下图:

20220326232733
20220326232733

1.3 TypeScript Playground

如果我们想要验证 ts 的一些 demo,有两种方法:

  1. 一个方法是本地编辑器写一个 ts 文件,然后手动编译看情况。缺点:本地需要搭建编译环境,每次写完要手动执行编译。
  2. 推荐:使用 ts 官方提供的 playground在线编译的网站,对应的链接可以去 ts 官网点上面的 playground菜单。能快速运行并随意切换版本和编译环境,非常方便。

20220326232745
20220326232745

基本介绍介绍完了,其实对于大多写 ts 的同学可能都有一些感觉,就是我看文档和大家的一些介绍,比如上面说的 : 操作符,都觉得自己基本能懂了。但是,文档的demo 中的例子,经常是什么 foobar或者 FishCat什么各种动物继承来继承去的,也不知道实际场景应该怎么用。然后总觉得对于高级语法不熟,所以一上项目遇到红就只能想到 any

20220326232758
20220326232758

实际上呢,每个刚开始接触的小伙伴都会有同样的问题,个人经验的一个解决方法就是平时多写,以及尽可能的避免写 any(废话)。那么怎么避免呢,一点小建议。平时有空的时候,可以去看看常用框架的一些 typwscript 的版本定义是怎么定义的,去做一些学习。以及如果实在不会的话也可以找小伙伴们一起讨论讨论。

所以,这里我们接下来就讲解,在 ts 中,一些可能会用到的小技巧。

二、常见技巧

20220326232812
20220326232812

这里会分成 特殊类型类型断言,和类型守卫三个方向来详细介绍

2.1 特殊类型

any / never / unknown

先介绍三个在 ts 中特殊的类型,这里还是做个基础了解:

20220326232854
20220326232854

  • any:任意类型,不受任何约束,编译时会跳过类型检查,不推荐使用
  • unknown 是某些值的集合,任何值都能冠以类型 unknown。这就是为什么 unknown 被称为顶端类型。
  • never:用于永远不可能发生的情况,主要场景用于错误检查或收敛条件类型,所以我们一般叫他底层类型。

【unknown 的使用】

看下面的图,,我们定义了一个 any 类型的 age,和 unknon 类型的 unknownAge。接下来,我们把 any 类型赋值给 number 类型(对应 number1) ,是没有任何问题。但是,我们把 unknown 类型赋值给 number 类型(对应 number2),就会报错。

20220326232932
20220326232932

不过反过来,所有类型都可以赋值给 unknown,一般我们会跟条件语句结合。比如下面的 number3,虽然 unknownAge 的类型是 unknown,但是他经过条件判断的推导之后可以知道他一定是 number 类型,因此 nunber3 就会被自动推导出是 number 类型。

这么说,可能还是有点绕,毕竟是随手举的小例子,也不知道实际应用场景,我们再继续看看。

20220326233036
20220326233036

假设我们有个神奇的函数,里面可能会有数字的操作,会有字符串的操作,也会有数组的 push 操作,甚至更多其他未知的类型。对于这种神奇的函数,我们一般建议是补全所有,但是如果不补全我们写 any 的话呢?可以看左上方的图,语法检查是不会报错的。不过懂 js 的都知道,这样直接丢去浏览器执行肯定是会有问题。

因此我们可以把参数的类型定义成 unknown,这时候,ts 的静态检查会报错。因为 math 方法的入参期望传递一个 number 类型,这时候你传了 unknown 类型,因为 unknown 是不能赋值给 number 的,所以这里就会报错。那要怎么改呢?从直觉上来说,肯定得每次调用之前检查下。比如右图, if 条件语句之下,参数就会被自动赋值成对应的类型,这样代码就有了更进一步的保障。

这就是通过 unknown 来避免代码中可能漏校验的情况。

【never 的使用】

never 呢,用于不会发生的场景。接下来还是看实际例子,这是将 never跟条件语句中结合的小 hackif else 之后的判断,就是应该永远不会走到的地方,所以他可以用于收窄类型来防止代码不小心到这里的检查。

20220326233119
20220326233119

一个实际例子(见上图)。假设有个 Icon 组件,他接收的参数是 reactElement 或者一个 config 配置,我们在最后的 else分支加一个 never 类型的 check,在当前情况下是永远走不到这个分支的,所以这里的值就是 never没有任何问题。 然后呢,某一天觉得每次写配置太麻烦了,期望能直接传递一个 string 表示 url,例如右图 IconProps的定义。那么下面的这个 nevercheck 就有用了,如果忘记处理这个新增的 string 类型,ts 检查失败就编译失败那么就会立即发生错误。

另外一种场景,可能是更常用的场景,就是作为泛型参数的默认值去支持内部逻辑处理,或者与 infer 和条件语句相结合,来实现去除某些属性的逻辑。

20220326233149
20220326233149

比如上图中, ts 官方的工具类型中,获取参数的方法 Paramters,抽取相交的属性 Extract 方法等,内部都有 never的身影。还有新一点的 ts版本,其中的 Promise / Awaited 等方法也都有使用。这里就不做进阶的讲解,初步能知道有这么个情况就行,时间有限,有兴趣的小伙伴可以自行深入学习泛型相关知识。

总的来说,上面讲解了三种类型,我们应该怎么选择呢?

  1. 在那些将或既不能取得任何值的地方,使用 never,比如抛出异常错误的场景,或者对于觉得需要进行严格的分支检查的场景,比如通用工具库
  2. 对于 unknown,我们在不确定类型的时候,可以先指定一个它的类型为 unknown,然后在根据后面的判断把它转化为想要的子类型。在其他的强类型的编程语言中,也会有类似的场景,叫做强制类型转换
  3. 而对于 any,除非你有意忽略类型检查,尽量不要使用 any

20220326233206
20220326233206

readonly 介绍

readonly 类型,顾名思义就是只读的类型,当你把参数设置成此类型的时候,然后尝试做赋值操作 ts 就会报错。例如下面的图中的数组,可以读其中的参数,但不允许使用 push 方法。

另外,对于 class 来说,如果你写了 getter 没有写 setter,那么解析器也默认会自动推导出 readonly 属性。

20220326233238
20220326233238

实际场景呢,一开始可能不一定能想到,但是日常我们经常会接触。比如 react 中,propsstate 都是 readonly的。因此,你直接给他们赋值会报错,只能通过 setState这种对外暴露的方法操作。

一个小注意事项,小写的 readonly 关键字只能标记普通属性比如字符串数字等,而首字母大写的 Readonly 加上泛型,则是用于标记整个对象数组等的转换。

20220326233251
20220326233251

null 空值

最后一个特殊类型,就是 null 空值,这个在 js 中也有此类型。

20220326233313
20220326233313

一个比较经常混淆的点在于,null 是空 undefined是未定义。因此对于一些特殊场景,需要确认某个参数一定要被传递的时候,可以使用 null 来代替 ?:。比如右图的 birthday,如果我写的是可选值,那么我在写一个空对象 {}也并不会报错。

但是如果我写的是 string | null,传入一个空对象,一般静态检查就会报错了。这通常用于对于某个参数需要他一定能存在的时候,即使可能在某种场景初始化的时候是空值。比如 reactcreateElement 方法中,props 默认就是 null 类型,毕竟你不能读取他的时候是个 undefined 吧。

2.2 类型断言

非空断言 !

最常见的非空断言,在表达式后面加个操作符操作符 ! ,可以用于断言操作对象是非 null 和非 undefined 类型。

20220326233426
20220326233426

比如上图,我们有个 logString 的方法,他的参数只接受 string 类型。另外我们还有一个 log 的方法,他的 messagestring 或者 undefined 或者 null 类型的。

这时候我们直接调用 logString 方法肯定会报错,因为 log接受的参数和 logString 接受的参数不同,这种情况通常出现在可选参数上。但是如果我们在一些分支语句内,可以保证变量一定不为空,就可以手动在变量后面添加感叹号。

不过需要注意的是,我们使用非空断言可以通过 TS 的检查。但在生成的 ES5 代码中,! 非空断言操作符会被移。所以在浏览器中执行以上代码,在控制台会输出 undefined。因此,如果我们没有十足的把握,尽量不要在代码中使用非空断言。

另外,在 TypeScript 2.7 版本中引入了确定赋值断言,允许在变量声明后面放置一个 ! 号,从而告诉 TypeScript 该属性一定会被赋值。为了更好地理解,还是结合具体的例子:

20220326233435
20220326233435

左边我们定义了一个全局变量 x,然后通过函数调用的形式去给他赋值。但是通常来说 ts 来说并没有那么智能,因为是在下面的函数内部,识别不出 x 其实已经被赋值了。于是编译器会报错,x 还没赋值就被使用了,觉得这是不对的。

但是,作为开发者,我们这里是能明确他肯定会被赋值的,一种方法是在使用的时候,像是上面讲解的断言一样,console.log(2 * x) 中的 x后面,加一个感叹号。但是如果很多地方都要用 x 变量,每个地方都写个感叹号很不优雅。因此我们也可以,在声明 x 的时候,就给在声明后面就加上,表示 x 值一定是有内容的,不会是 undefined 或者 null

还有一个很常见的场景,就是在 react 中,我们如果有个 ref 的引用,然后在组件 mount 后才读对应的值。例如下图:

20220326233444
20220326233444

这个时候如果你直接读,是会提示 ref 的对象可能为空,但是从你强大的经验判断他绝对不可能是空,因此可以加个感叹号表明自己的信心。就可以少写个 if 判断语句看起来会简洁很多。

类型断言 as 和 <>

刚刚说完了非空断言,是表示需要确认所用的内容不是空。那么如果我要断言为某种类型呢,比如说把 string 想强行转成 number,或者 A 类型转成 B 类型。这时候就要用到 as 关键词,或者尖括号关键词了,意思都差不多。最常见的场景,比如我们在做一些 dom 操作的时候,确定某个元素是一定存在的。

20220326233541
20220326233541

如果我们像第一行那么写,ts 直接推断出来的是 HTMLELement,毕竟作为静态解析去,你要他智能的根据 id 或者 classname 推断出元素类型也太为难了。所以这时候如果假设是个 checkbox 的按钮,你想访问他的 check 属性就会报错,提示 checked 不一定存在于 html 中。即使加了感叹号也没有,因为这里并不是空导致的错误。

那么解决方法,就可以使用 as 来断言,告诉解析器这是一个什么类型的元素。比如上图中的 checkbox1,强制告诉他是 input 类似的 element,然后我们再读取 checked 属性就不会报错了。下面的 button element同理。

除了 dom 元素,还有一个场景,就是对于 ts 这种静态检查无法推断出来的内容。

20220326233557
20220326233557

比如上图的左边,我们给文本框 props 属性设置了默认的 status,但是在实际组件内读取的时候,因为 props 中定义的 status 是可选的,因此按照常规的类型推导他可能是空。

一种方法,就是我们可以使用 ! 的断言来表示他一定存在,可以见左图下方的注释。另外一种方法呢,就是对于 defaultProps 使用 required 定义一下表示这些也是必须的,然后在使用的时候使用 as 将内容指定为 defaultProps。这样,如果我在写代码过程中,真的不小心把某一种应该默认赋值的内容没有给赋值,就会报错。可以一定条件避免一些风险。

再有,右图中的 list,里面可能有多种类型的数据,我们使用 filter过滤函数的时候,期望 result 被推断成 A[],但是实际因为 item 的确是 A | B 类型的,他也推断不出来,因此我们可以使用 as 来进行强制转换。

另外,还有两种常见的场景。比如有时候我们对一个空对象进行操作确认他没什么问题,可以给强行制定类型。通常用于写单测。

20220326233612
20220326233612

以上说的 as 都是对一些对象进行操作。但如果像是最开始说的,想把 string 强行转换成对象类型的话,你直接写 as Person 会发现还是会报错,不允许将 string 指成 xxx

但是有一个骚操作,可以使用双 as,强制先转成 any 再转成其他类型。注意,这种做法是强烈不建议的,大家只是知道有这种操作就好。毕竟这只是 ts 静态检查时的语法,代码一编译,运行时还是可能报错的。所以 as 可以让我们指鹿为马,但是如果一不小心指错了就容易线上 case 了。

最后一个场景,是 ts 3.4 之后的新功能,叫 const 断言。可以继续看一下下图的写法。

20220326233631
20220326233631

假设我用 js 写了一个对象,状态里面有值 successerror。如果我们正常写代码,Status 里面的对象 ts 自动推断的类型是 number 类型,因此在代码使用过程中直接去修改,没有任何语法上的错误,所以大多时候我们会用枚举类型来做一次定义。

而这里讲的就是一个新玩法,用 const 断言来替代,我们直接从 js 的语法写完对象之后,最后加上一句 as const,就表示这个对象里面的内容都是常量类型,不允许你进行修改。如果强行要修改就会出下右边图片的报错。这种能解决大多我们使用枚举的场景,除了二进制。

2.3 类型守卫

说完了类型断言,我们接下来说一下类型保护,也叫类型守卫。他允许你使用更小范围下的对象类型,从而使类型定义更加准确。我们可以有一堆类型谓词来辅助我们去做类型保护,接下来还是以具体例子。

is 关键词

上面我们讲解了,不用类型的数组使用 filter 功能的时候,对于结果需要使用 as 来强行标记对应的结果类型。这么做其实是有个缺点的,就你每次使用这个函数,都要用 as 判断下。我们还有个解决方案,用 is 关键词。比如下图

20220326233649
20220326233649

我们对 isA这个函数中的参数 item 给定义成 A 类型,那个下面再使用的时候,item就会被识别为 A,而不是括号里面定义的 A | B,这样 filter 返回回来的就是 A[]。在这种场景, is 有点像对于函数参数的 as 的感觉。

查找类型 []

20220326233726
20220326233726

假设我们有个 Person 类型的接口,里面有 nameageaddress 等字段(具体可以看图)。然后,在业务中另外一个地方需要预览用户信息,数据都是同样的数据,但是他只需要 nameaddress 这两个字段。那么这时候,如果我们不想每次遇到这样的场景都去写一遍,还可以这么做呢?

通常的做法,我们可能会抽象一层,把 Address 单独抽出来当一个类型,然后下面接口书写的时候,对 addr 字段赋予抽象出来的 Address 类型。这是常规的做法,那么有没有偷懒的做法呢?

js 中是可以用 a.b.c 去访问对象里面的子内容或者 a['b'],而 ts 的写法只能是是 []。所以就很明确,对一些重复字段的抽取,就可以像下图一样,nameaddress 直接读取 userInfo 中的值,这样无论 usserinfo 中的字段如何做扩充都不用怕了。

20220326233747
20220326233747

然后我们再继续拓展一下,如果说有很多个字段,或者我现在都要定义一遍然后取一遍值,感觉有点麻烦。有没有什么办法可以让我循环的去取呢?这个时候就可以说一下 in 关键词,跟 js 中的 in 一个意思,然后结合 [],可以实现更加精简的写法。

20220326233755
20220326233755

当然,这想法大家都有,于是官方就很有先见的已经把上述的功能封装成了泛型工具 Pick,大致原理差不多,通过泛型接收原始类型和需要提取的字段。当然,如果你需要 pick 的字段太多,建议还是用 extends 或者另想更好的代码组织方法。

typeof 获取属性类型

前面说完了 isin,接下来说一些更加实用的工具关键词 typeof。跟 js 的差不多,jstypeof 是判断一个 js 变量的类型,而 tstypeof 就是自动推导 ts 变量的类型。

20220326233805
20220326233805

比如上面我们写了一个 option1 的变量,他是一个包含 timeoutnumber 的对象。然后我们假设其他地方有 option2,也需要用同样的定义,这个时候除了引用 Optioninterface,还可以更直接的直接使用 typeof defaulOption ,也可以自动推导出 ts 的类型。

更常见的一个写法是像右边图所示,我们可以先写一些常量定义,然后使用 typeof 自动推导。对于一些历史项目刚尝试改造成 ts 的场景,这种方式可能会更加容易接受也更加方便。

keyof 关键字

接下来讲一下 keyof,跟 js 中的 Object.keys 略有类似,同理这个是对 ts 中的 interface 做处理。

20220326233815
20220326233815

第一点,用 keyof 去获取某个对象声明的 key。比如图片的最上面,一个 User 对象里面有 nameage 字段,keyof User 就是取 key 值,为 nameage

那么在实际场景应该怎么用呢?一个很容易遇到的场景,取一个 object 对象中某个字段的的值,按照 js 的逻辑,左边的函数 getPersonInfo并没有任何毛病。但是从静态类型推断来说,他并不知道 name 是不是 js object 中的一个 key,也不知道对象里面是什么东西,所以返回类型只能是 any

这个时候,我们就可以把 keyoftypeof 结合,对参数 key 的类型给设定为 keyof typeof user,也就是表明是 user 中的一个 key。这样对于 userNamets 就能知道是 string 或者 number 类型了。

但是,我们对 userName 的期望应该是 string 类型,那么应该怎么做呢。这里留一个思考作业,可以结合后续的讲解来参考(提示,使用泛型)

20220326233827
20220326233827

另外,有时候我们如果要把某个对象里面的内容全部转成另外一个类型。比如把上面的 Person 对象中的字段,全部变成 number 类型。可以跟in 关键词一起使用,上面在讲 in 的时候应该有一定的介绍,因此不做过多讲解。

这里介绍一个实际项目中,利用 keyof 可以做的一个小功能。对于 api 中心化的管理。

20220326233837
20220326233837

比如 get 请求这里我们入参是 url,然后通过泛型定义 urlapi 中的一个字段。这样子我们在调用 get 方法的时候,编辑器就能自动提示。如果有写错的场景,也会做一定的报错。

三、泛型相关

3.1 基础语法介绍

20220326233853
20220326233853

基础使用:使用尖括号表明泛型的名字,通常结合函数或者接口等一起使用。参考上面的图片,我们需要实现一个 reverse 函数,对于 js 来说,数组里面的内容是不确定的,可以是任意类型。但我们期望有个约束, reverse 函数的返回值也是个数组,它和传入的数组类型是相同的。

当我们调用 reverse<Number> 的时候,Number 类型将会填充任何 T 出现的位置。因此,我们把 T 被称为类型变量。例如上图的,当 T=number 的时候,函数的参数 items 就会变成 numbers 数组类型,他的返回值也同样为 number 数组类型。

这里有常见我们对泛型的常见命名,按照惯例,类型参数名称是单个大写字母,区分类型变量和普通类或接口名称之间的区别:

  • T(Type): 最常用的类型参数名
  • K(Key): 对象的键类型
  • V(Value): 对象的值类型
  • P(Property): 对象的属性类型
  • R(Result): 类型推导的结果类型

那么泛型在什么场景中我们会用到呢?这里是一个实际的例子。

20220326233913
20220326233913

对于使用 React 的同学,看到这个代码应该会很熟悉。这里我们写了一个 Button 组件,他外部传递的 props 参数为 size。但是另外,React 也有一些自己的参数,比如组件里面都可以访问 props.children,来渲染子元素。这里的核心原理其实就是通过泛型进行的一个组合,React Props 暴露出来之前,内部将自己特有的一些属性,例如 children 跟传入的泛型进行一个组合。

再有,大多业务,我们的后端对接口请求的返回的内容会再封装一层,比如 code,message,result

20220326233923
20220326233923

可以看左边的图,如果每个方法都这么写就会很累,这个时候可以考虑利用泛型,类似函数的封装的感觉。毕竟实际写代码使用的时候,我们一般也会有个中间层去处理通用的错误。例如 code200 的时候就只取 result。所以我们可以通过泛型定义包装一层使用,回来的 result 就能确认是 number 类型,而不是每次每个接口都需要重新定义一遍 code,message 等字段。

上面是两个很具体的例子,可能大多同学都会写,觉得自己也会。但是江湖一直流传着 ts 的体操,想知道要怎么做。

20220326233932
20220326233932

这里我们接下来会简单介绍下常用的两种在泛型中的小工具,无所谓是不是体操,项目也可能会用到的。

泛型条件 T extends U? X: Y

从简单的入手,我们先看泛型的条件,这是在 TypeScript 2.8 中引入的类型。跟 js 的三元运算语法基本一致,如果 T 是 U 的子类型的话,那么就会返回 X,否则返回 Y。

20220326233947
20220326233947

我们看图中的第一行,a extends abc,左边的 a 是字符串类型,右边的 abc 也是字符串类型,这种情况下 extends在语义上可理解为 js 中的三个等于号。下面的 result2 也是一样,不过是数字类型。

那么如果是对象呢,extends 左边的东西比较多,右边比较少。可以想一下,这里的 result3 的类型是什么?答案是 true,因为左边可以继承右边而得出的,一个简单的记法,就是我们对象的 key 越多,定义的范围其实就越窄。

一般,简单项目直接的场景可能不多,但是可以基于他的特性实现一些工具函数。比如 ts 中自带的 ExcludeExtreact。上面是他们源码的写法,主要接收两个泛型参数,这里用 TU来替代。

20220326233954
20220326233954

看一下实际使用的效果,Exclude 顾名思义,就是从类型 T 中取出不在 U类型中的成员,例如上图,我们有 String1String2 两个类型。当我们运行 Exclude<String1, String2> 以后,就相当于从类型 String1 取出不在 String2 的元素。一个个的看,astring2 里面不用取,b 不在 string2 里面要取出来,c 也不在取出来,所以上述中 exclude string1 string2 的结果是 b | c

另外还有一个类似的方法,叫 Extract,从类型 T 中取出可以分配给 U 的类型,类似于取两者的交集,结果就是 a,这里就不做更细致的讲述。

3.2 类型推断 infer

讲完了泛型中的条件之后,接下来讲解下泛型推断 infer,也是泛型中的一个重要概念。infer 表示在 extends条件语句中待推断的类型变量,简单的例子可以看图片。

20220326234006
20220326234006

整句的意思表示,在这个条件语句 T extends 了一个函数,简单起见我们可以把这个函数叫 funcA,因为我们的 T 是未知的,所以 funcA 的内容是什么我们也是未知的。对于这个未知的 func,需要起一个名字吧,比如他的参数可以是 a,可以是 b,那这里就是假设他是 P,为了让编辑器知道这是个假设,我们就加一个特殊标记叫 infer,表示 P 是推断出来的。这么说了一堆,可能有点绕,所以我们还是结合实际例子来看。

20220326234016
20220326234016

这里有一个 User 类型,然后我们有个 getAge 方法,根据用户的信息获取他的年龄,所以 getAge 函数的入参参数是 user,类型是我们上面定义的 User

当我们执行 ParamType<GetAge> 的时候,GetAge 作为一个函数,符合上图中 FuncA 格式,因此 P 就是这个函数中参数的类型。因此图中,type A 的类型为 User。对于 typeB,我们这里泛型传入的是 string,他并不符合 funcA 的格式,因此返回的是传入的泛型 T,也就是 string 类型了。

另外,在 Reactts 定义中,其实我们经常也会看到 infer 的出现。就拿 useReducer 来举例子,他的第一个参数是 reducer,第二个参数是初始化参数,也是第一个参数中回调函数中的第一个参数类型

20220326234027
20220326234027

比如上边的例子,我们定义一个 reducer,参数是 number 类型。然后下面使用 useReducer 的时候,第二个参数传递的是空字符串,那么编辑器一般会报错,提示他应该是 number 类型。这里如果有兴趣的小伙伴可以往下翻一番,就可以看到也是用到了 infer 的关键词。

当然,还有更多条件语句,继承,infer 等相关功能组合使用,去做更复杂的操作,甚至能用 ts 来下棋。不过,这里是小技巧的分析,因此更多的就交给后续的体操课程。上面的学习已经基本足够我们项目中日常使用了。

20220326234034
20220326234034

总的来说,泛型的基础入门虽然简单,但是通过各种类型组合,是可以玩出花来的。基础入门简单,但是要玩得好,想要能解决项目中遇到的每个问题,需要不断的磨练。

20220326234055
20220326234055

所以,我们接下来,还是先把工具函数看熟,不追求能解决每个场景能随手写个象棋,但是能覆盖大多场景,能让业务真的使用上有帮助。

20220326234104
20220326234104

3.3工具函数

最后一点时间,我们来介绍下 ts 中,泛型常见的工具函数。这些都是 ts 已经帮忙实现好的,比如前面已经接触到的 ExcludePick 等,这些大多在 2.8 版本中已经实现。另外还有在 3.3 有一些小的追加,以及有几个内置的字符操作类型,这些都可以在 ts 的官网中,或者我 ppt 上的链接点进去就可以看到详细介绍。我们快速过一下常见的工具函数以及使用 demo。

20220326234115
20220326234115

Partial / Required / Readonly

首先 Partial,可以讲传入的参数变成可选。

20220326234127
20220326234127

比如图中的 user1,有一个 name 属性是必填的。经过 Partical 转换之后,就会变成可选。这在写组件 props 参数处理的时候非常有用。

相反的,有把必填变成可选,当然也有把可选变成必填的。就是 Required 方法,使用方式和 partial 一致,比如右图,name 是可选,经过转换后就可以变成必填。

再有,Readonly 故名思义,就是把所有参数变成只读,这个在前面介绍的时候也有讲过:大写对对象类型等做转换,小写对基础类型。

Record

除了上面说的转换函数,接下来还有 Record 函数,它是能用来快速定义一个对象的 keyvalue 类型的。

20220326234541
20220326234541

比如上图的,假设我们有个 CatInfo 的定义,然后也有一堆 catName 的定义。我想快速的将这两个类型进行组合,变成一个 map 对象,keycatName,值是 catInfo,这时候就可以搬出 Record 方法,经过转换后就可以看到对应的输出。

这时候,如果你往 map 不小心新添加了一只外来 cat,编辑器就会有友好的提示表示不开心 :)。

Pick / Omit / Exclude / Extract

了解完 Record,接下来我们继续介绍几个类型功能的工具方法。

20220326234531
20220326234531

Pick,他是用来从一个类型里面去提取某一部分类型的东西。对 lodash 熟悉的小伙伴们可能知道,lodash 中也有个 pick 的方法,功能类似。他接接收两个参数,第一个是原始数据类型,比如上面的 Todo,第二个参数就是需要提取的函数,比如 title completed。运行之后,原来 Todo 里面是有三个 key 的,提出来两个就是只剩两个了,也有点像是数组的过滤方法。

还有几个类似的,Pick 是抽取出来,那肯定有删除对于字段的操作,就是 Omit,跟 lodash 中的操作也同理。pickomit 主要操作对象。对于基本数据类型,比如 stringnumber,我们就可以使用 ExcludeExtract,这里在前面介绍条件语句的时候也有涉及。这些在做接口返回的时候,数据处理场景会很有用

函数相关工具

最后,还有 Parameters,把函数的参数作为元组返回。

20220326234521
20220326234521

例如上面的 sumFunc,传入的参数是 ab 都是 number 类型。那么 Parameters sumFunc 就是[a: number, b: number]

这一个是获取参数。那么能获取参数肯定也能获取返回值,就是 ReturnType 了。比如上图的 T4,获取 SumFuncreturnType 类型就是 number。这里我们用的是 typeof sum,其实他的值就跟上面定义的 sumFunc 一样,实际项目中有时候懒得特意给函数定义类型和返回值,也会这么写。不过需要注意,不是所有场景都能靠 ts 的静态类型推断出来返回值,所以该要写定义的时候还是需要写一下的。


时间有限,大概就先介绍这么多。这些都是基本操作,具体对怎么实现原理有兴趣的小伙伴呢,可以点进源码看看。都不用紧张,基本每个工具函数没有超过十行的,都是上面讲的一些语法的组合,啃一啃都能啃得动,然后就可以慢慢成为 ts 大佬了。

20220326234506
20220326234506

四、其他 & 总结

最后,分享一些在 react 框架中特定的一些类型。为什么没有 vue 的呢,个人不是很熟项目写得太少了 :(

4.1 React 中的常见定义

首先呢,jsx 是一个全局的命名空间,不同的库可能会有不同的实现。而在 react 中的 jsx,对应的基本就是 react element,都是含有 propstype 属性的对象,然后 reactnode 是多种类型的集合。具体关系可以看图

20220326234242
20220326234242

除了上面说的元素节点相关的定义,还有很多事件类型和 css 类型的定义,这里汇总了一张表供大家参考,也是可以直接看图

20220326234252
20220326234252

最后,就是有时候我们项目中引用了第三方库不一定是用 ts 写的,那么我们作为 ts 的代码仓库应该怎么办呢?大多场景,如果是一些常见的第三方库,比如 jqueryaxios 等,都会有 ts 的定义版,可以去 type search 网站,或者 npm 网站搜 @types/xxx,寻找是否有支持的包,如果有支持的,npm install进行下载即可。

但是呢,如果有时候真的遇到了小众包没有写的,这时候就需要自己写一下定义了。方法是新建一个 .d.ts 结尾的文件在项目中,通常建议放在 types目录下。然后手动 declare module 以及手动书写对应的方法即可。

20220326234302
20220326234302

4.2 THE END

总的来说最后,我们来回顾一下本文内容: * 初识:ts 基本语法、类型怎么写 * 基础:特殊类型、断言、类型守卫。各种实际小栗子 * 范型:基础和相对复杂点的泛型,以及常见的工具函数介绍和使用

总的来说,Typescript,路漫漫其修远兮,大家加油。然后这里是我个人的一个微信群,和微信,有兴趣的交流。

20220326234331
20220326234331