下半年开始准备all in ts
, 在越来越多的项目中,开始使用 TypeScript
之后,越来越觉得预定义类型及类型推断的重要性及其带来的好处。只需要在声明变量时声明其可能的数据类型或结构,后续关于它的一切都交给 TypeScript
的静态类型检查及推断,在编码阶段就能避免大量潜在的错误。
下面typescript
简单称ts。
1. ts之基本类型
一般来说,我们常用到以下基本数据类型:any
boolean
number
string
undefined
null
object
Array
Function
等。为了更具体的描述对象(object)的数据结构,也常使用接口。接口使用关键字 interface
,通过它定义一种对象数据结构:
1 | interface IPet { |
接口能够通过关键字 extends
对数据结构做继承拓展,甚至能够同时拓展自多个其他类型(多重继承):
1 | interface IAnimal { |
上例中,IAnimal
类型的 favorites
属性被标记为可选的(?),这意味着在进行代码检查时,该属性可能存在也可能不存在(undefined)。
符号 ?
是关于属性描述的一种修饰符。
2. ts之高级类型
有时某个类型只是其他类型的别名,可以使用 type
关键字来定义类型的别名,方便后续使用:
1 | type NumberArray = Array<number>; |
在上面的例子里,数组类型 Array
实际上接受另外一个类型(例如 number
, 或者是我们自定义的 IDog
类型作为参数,然后返回一个全部都由该类型组成的数组)。这使得 Array
具有了抽象任何其他类型作为数组的能力。
这个“类型参数“ 在 typescript 中称为泛型(generic)——一种泛指的类型变量。
既然拥有了类型变量
,倘若结合一定的逻辑运算,是否能创造更多的可能性?ts
强类型选择,提供了类型推断的种种可能,使其对实际代码的描述能力大大提高。
以下我们通过一些例子简要的了解一下这种“面向类型的编程”。
2.1 ts之并集 union
并集) 是若干个集合所包含的全部元素组成的集合。
例如,集合 a = [string, number, boolean]
与集合 b = [string, boolean, Function]
的并集,是集合 [string, number, boolean, Function]
, 并集中已包含集合 a
与 b
的全部成员。
换句话说,并集中的某个元素,既可能是 a
的成员,也可能是 b
的成员。
在 ts
中,使用或运算符 |
代表这种关系,例如:
1 | // value 既可以是 number,也可以是 string, 它的类型是 number 与 string 的并集 |
2.2 ts之交集 intersection
交集) 是若干个集合都共有的全部元素组成的集合。例如,集合 a = [string, number, boolean]
与集合 b = [string, boolean, Function]
的交集是集合 [string, boolean]
, 交集中的每个元素既是集合 a
的成员,也是集合 b
的成员。
在 ts
中,使用与运算符 &
代表这种关系,
例如:
1 | // IAppleBanana 具有全部的三种属性 name, color, length,因此它既可以认为是 IApple,也可以认为是 IBanana, 是 IApple 与 IBanana 的交集。 |
注意,对于接口,{ name, color, length }
是 { name, color }
的子集,因此接口的交集是接口包含的全部键的并集组成的类型。
2.3 ts之类型索引
ts
提供了关键字 keyof
以获得一种类型(通常是接口) 下所有的键构成的集合。
例如:
1 | // 声明接口,满足该接口声明的对象必然具有 x, y 属性 |
上面定义的方法 getValueFromPoint
声明了只能针对 IPoint
对象使用。但配合 泛型
,可以实现一些更通用的类型声明:
1 | // 只使用一个泛型 T |
2.4 ts之类型映射 Mapped Types
ts
提供了关键字 in
用来约束类型是否属于某个类型集合。配合类型查询,可以创造出一些用于产生衍生类型的工具类型:
1 | // 将泛型 T 的所有键都标记为非必须(?) |
于是,我们拿到了以下辅助类型:Partial<T>
, Required<T>
, Pick<T, K>
。
2.5 ts 之 条件类型
ts
拥有一定程度的类型运算逻辑。类似于三目条件运算符 condition ? a : b
, 可以对某类型(泛型)进行类型条件运算推断:
1 | // 类型 FruitType<T> 实际类型由泛型推断而来 |
把这种逻辑结合泛型应用在并集类型上,可以衍生出一系列颇为实用的工具类型:
1 | // 定义两个 Union 类型 |
再进一步,利用前面得到的 Pick<T, K>
和关键字 keyof
,可以声明以下类型:
1 | // 与 Pick<T, K> 正好相反,从 T 中去除 K 包含的类型 |
通过以上的实例可以发现,ts
通过有限的类型关键字或运算符 interface
type
extends
|
&
keyof
以及属性修饰符 ?
-
!
等,结合 “类型的变量——泛型”,衍生出了一系列非常实用的工具类型,可谓是 typescript
里的 lodash
方法集了!
现简单整理如下:
工具类型 | 说明 | 已内置 |
---|---|---|
Partial<T> | 将泛型 T 的所有键都标记为非必须(?) | ✅ |
Required<T> | 将泛型 T 的所有键都去除掉非必须(-?) | ✅ |
Pick<T, K> | 从泛型 T 的所包含的键值类型中选择若干,并构成一个新的类型 | ✅ |
Omit<T, K> | 从 T 中去除 K 包含的类型 | ❌ |
Filter<T, U> | 从泛型 T 中过滤选择出能够满足 泛型 U 约束的类型 | ❌ |
Exclude<T, U> | 从泛型 T 中剔除能够满足泛型 U 约束的类型 | ✅ |
3. 高级类型使用
以下通过两个实际编码场景,来简单应用一下以上所学习的高级类型。
3.1 可选配置项
某模块通过参数 options 提供配置选项,供使用者配置时,这些配置选项通常都是可选的;然而在模块内部收到配置项时,也常使用 “默认配置项” 与使用者的配置项合并补全成完整的配置项。这样一来,对于外部使用者来说,options 的每一个属性都是可选的,而对于内部使用者来说,options 的每一个属性都已被补全,因此能直接使用。
一般的,可以定义类型 IOptions
并通过属性修饰符 ?
表明属性为可选;再在使用时,通过修饰符 !
标注该属性一定存在——这种做法虽可行,但是每次使用时都需要加 !
显得比较繁琐。这时可以使用上面提到的 Required
并衍生出类型 Required<IOptions>
来描述这种差异。例如:
1 | // 对外暴露的类型,指示用户配置项的每一个属性都是可选的 |
3.2 React 高阶组件 (HOC)
高阶组件(Higher Order Component) 在 React 开发中是一种常用的范式。通过创建一个能够在运行时,动态创建新的组件(类或方法)的方法,可以实现对原组件无侵入的注入(inject)属性或剔除(expel)属性的目的。
下面对组件 Box
进行 HOC 加工声明以阐述这种方法。
1 | import * as React from 'react'; |
3.2.1 属性注入(inject)
属性注入的本质是动态创建一个新的组件(类或方法),它除了接收原组件的全部属性并且把它们直接传递或间接加工传递给原组件之外,还根据需要另外接收一些类型的参数。假如原组件参数类型为 IProps
, 要额外添加的属性为 IInjectedProps
, 那么动态创建的组件的类型就应该是 IProps & IInjectedProps
, 并且在该组件逻辑内,仅传递正确的 IProps
给原组件以确保类型传递正确。
以下是属性注入的一份 typescript
实现:
1 | import Box from './Box'; |
3.2.2 属性剔除(expel)
与属性注入正好相反,属性剔除是动态创建一个新的组件(类或方法)并提供部分预设的属性,这样一来,生成的组件只接收原组件属性中的一部分,其他部分的属性对于使用者来说相当于是被剔除了——从另一个角度上看,也可以认为是对原组件预先注入了部分定义好的属性。另外,鉴于组件本质上是方法调用及参数列表,因此也可以把这种方式看作组件的柯里化(currying)。
以下是属性剔除的一份 typescript
实现:
1 | // 原组件 |
类似的,React v16.3+ 的 context
, react-redux
或者 mobx
的 store 属性注入等,也可以用类似的方式来描述。一般来说这些第三方类库已在其配套的类型定义文件中声明了注入方法的原型,不需要自己再手动编写。
注:在最近的几个版本的 typescript
中, 对 react 组件声明注入使用的工具类型可能存在 bug, 导致 ts 编译报错 issue#28748;本文中的例子,并不保证编译正确,仅阐述高级类型的使用方式。
4. 总结
Typescript
的高级类型为复杂的类型表达和推断提供了更多便利和可能。对于强迫症患者来说,为了能自然的使用类型,增加了不少学习和维护成本,堪称为“面向类型编程”也不为过。但它并非完美无瑕无懈可击,在复杂性增加后,也存在各种BUG(尤其是配合 React 生态使用时),非常影响开发体验——也许适当的灵活使用 as
关键字绕过复杂的类型推断反而更实际。