探索 TypeScript 类型注解 - 类型编程
Exploring TypeScript Type Annotations - Type Programming
作者: zhilidali
欢迎来到 《探索 TypeScript 类型注解》 系列教程。
上一篇介绍了 TS 的高级类型。
本篇将前面的知识点融会贯通,将对类型的探索提升一个层次:从类型层面进行编程。
目录
类型编程
首先,我们回顾一下前几节对类型的探索。
- 通过数据类型,我们了解到 TS 支持的数据类型。
- 支持 JS 数据类型。
- 新增一些数据类型。
- 通过自定义类型,我们了解到如何声明或命名一个类型。
- 接口,
interface
来声明各种复杂数据类型。 - 类型别名,
type
对类型进行命名。 - 泛型,
<T>
如同 JS 中函数将类型作为参数传递。
- 接口,
- 通过类型检查,我们了解到类型与 JS 之间的各种交互。
- 类型推断,通过 JS 的值推断出相应的类型。
- 类型断言,直接对 JS 中的值指定一个类型。
- 类型兼容,由类型之间的关系反映出 JS 值之间的关系。
- 类型保护,通过 JS 中的操作符来反映出精确的类型。
- 通过高级类型,我们了解到 TS 提供的各种类型操作符。
- 交叉类型,使用类型操作符
&
- 联合类型,使用类型操作符
|
- 索引类型,使用类型操作符
keyof
,T[K]
- 映射类型,使用类型操作符
in
- 条件类型,使用类型操作符
T extends U ? X : Y
infer
- 另外还有用于类型保护的类型谓词中的
is
操作符以及上面所介绍的typeof
- 交叉类型,使用类型操作符
根据 TS 提供的 数据类型 以及声明的自定义类型,结合高级类型中的 操作符 可以对类型进行各种运算 (高级类型本质上就是各种操作符表达式)。
再加上具有函数功能的 泛型,可以对类型的运算进行封装、复用、组合。要知道函数是 JS 中最强大的武器,谁说“类”来着,算了,好累,我还要(搬砖)拯救世界。还要知道,TS 没有采用传统面向对象语言使用的名义类型,而是基于偏向于函数式编程的结构类型,(JS 是多范式编程语言)。
到此为止,我们已经具备了对类型进行编程的各种工具 (程序 = 数据结构 + 算法),接下来各位童鞋就可以发挥无穷的智慧了。
童鞋请留步!俗话说吃人嘴短,拿人手软,请先让巨硬(微软大大)炫个富。下面介绍 TypeScript 官方标准库中封装的实用工具类型。
实用工具类型
TypeScript 提供的实用工具类型用来实现常见的类型转换,这些类型工具函数是全局可见的。
Extract,Exclude,NonNullable
-
Extract<T, U>
:从T
中提取可以赋值给U
的类型 -
Exclude<T, U>
:从T
中排除可以赋值给U
的类型 -
NonNullable<T>
:从T
中排除null
和undefined
使用示例
type foo = Extract<number | string, string>; // string
type bar = Exclude<number | string, string>; // number
type baz = NonNullable<number | string | null | undefined>; // string | number
具体实现
// 主要使用条件类型 `T extends U ? X : Y` 实现
type Extract<T, U> = T extends U ? T : never;
type Exclude<T, U> = T extends U ? never : T;
type NonNullable<T> = T extends null | undefined ? never : T;
Partial, Require, Readonly
-
Partial<T>
:将T
中的所有属性设置为可选 -
Require<T>
:将T
中的所有属性设置为必选 -
Readonly<T>
:将T
中的所有属性设置为只读
使用示例
interface Type { a: number, b?: string };
let foo: Partial<Type> = { b: 'b' };
let bar: Required<Type> = { a: 1 }; // Error
let baz: Readonly<Type> = { a: 1 };
baz.a = 2; // Error
具体实现
// 主要使用映射类型 `[K in T]: Type` 及索引类型 `keyof T`、`T[P]` 实现
type Partial<T> = { [P in keyof T]?: T[P] };
type Require<T> = { [P in keyof T]-?: T[P] }; // 注意这里的 `-?`
type Readonly<T> = { readonly [P in keyof T]: T[p] };
TypeScript 标准库中提供了许多实用的工具类型,而且随着 TypeScript 不断更新迭代,会有更多的实用工具类型加入到标准库中,此处不在重复介绍(提示:实用工具很实用),详情请移步官方手册,手册中给出了详细的使用示例。对于这些工具类型的具体实现,请移步官方仓库的 lib。
typeof
在 TS 中,还可以使用 typeof
来获取变量的类型。
let foo: number = 3;
type bar = typeof foo; // 相当于 type bar = number
extends
前面的章节中多处使用了 extends
关键字。如下
原生 JS 中类的继承
class A { a: number }
class B extends A { b: string } // B 继承 A
let a: A = new A();
let b: B = new B();
a = b; // Ok, A = B 少兼容多,子类兼容超类
接口继承
interface A { a: number }
interface B extends A { b: string }
let a: A = { a: 1 };
let b: B = { b: 'b', a: 1 };
a = b; // Ok, A = B
泛型约束
interface A { a: number }
let foo: <B extends A>(arg: B) => void;
foo({ a: 1, b: 2});
条件类型
interface A { a: number }
interface B { a: number, b: string }
type E = B extends A ? true : false;
// type E = true
汇总如下
- 类的继承:
class SubClass extends SupClass
- 接口继承:
interface SubType extends SupType {}
- 泛型约束:
<T extends U>
- 条件类型:
T extends U ? X : Y
以上均有共同的形式 Sub extends Sup
- 从
extends
关键字的语义:它们之间属于继承关系,即子类(型)继承超类(型)。 - 从类型兼容性角度:超类型兼容子类型,即子类型可以赋给超类型。
- 从功能上:
- 类和接口中的
extends
用来定义,可有多个超类Sup
,中间用,
分割。 - 泛型约束和条件类型中的
extends
用来检测兼容性,即Sup
是否兼容Sub
- 类和接口中的
类型与集合
既然是编程,下面从数学的角度来简单粗略地描述 TS 的类型系统,(读者可略过,想深入的童鞋可移步:https://zhuanlan.zhihu.com/p/38081852)。
TS 中的类型好比数学中的集合,类型是具有某种特定性质的 JS 值的集合。
比如 number
类型对应 JS 中所有数值的集合。
类型集合的分类
-
any
类型对应为 全集。 -
never
类型对应为 空集。 - 联合类型是类型集合之间的 并集。
- 交叉类型是类型集合之间的 交集。
结语
本篇主要是对类型进行编程的能力进行了梳理。
协议
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
《探索 TypeScript 类型注解》
- 扩展的 JavaScript
- 数据类型
- 自定义类型
- 类型检查
- 高级类型
- 类型编程
参考链接
Handbook: https://github.com/microsoft/TypeScript-Handbook
深入 TypeScript 的类型系统: https://zhuanlan.zhihu.com/p/38081852