探索 TypeScript 类型注解 - 类型编程

1,653 阅读6分钟

Exploring TypeScript Type Annotations - Type Programming

本文由 WowBar 团队首发于 GitHub

作者: zhilidali

欢迎来到 《探索 TypeScript 类型注解》 系列教程。

上一篇介绍了 TS 的高级类型。 本篇将前面的知识点融会贯通,将对类型的探索提升一个层次:从类型层面进行编程。

目录

类型编程

首先,我们回顾一下前几节对类型的探索。

  • 通过数据类型,我们了解到 TS 支持的数据类型。
    • 支持 JS 数据类型。
    • 新增一些数据类型。
  • 通过自定义类型,我们了解到如何声明或命名一个类型。
    • 接口, interface 来声明各种复杂数据类型。
    • 类型别名, type 对类型进行命名。
    • 泛型,<T> 如同 JS 中函数将类型作为参数传递。
  • 通过类型检查,我们了解到类型与 JS 之间的各种交互。
    • 类型推断,通过 JS 的值推断出相应的类型。
    • 类型断言,直接对 JS 中的值指定一个类型。
    • 类型兼容,由类型之间的关系反映出 JS 值之间的关系。
    • 类型保护,通过 JS 中的操作符来反映出精确的类型。
  • 通过高级类型,我们了解到 TS 提供的各种类型操作符。
    • 交叉类型,使用类型操作符 &
    • 联合类型,使用类型操作符
    • 索引类型,使用类型操作符 keyofT[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 中排除 nullundefined

使用示例

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

  1. extends 关键字的语义:它们之间属于继承关系,即子类(型)继承超类(型)。
  2. 从类型兼容性角度:超类型兼容子类型,即子类型可以赋给超类型。
  3. 从功能上:
    • 类和接口中的 extends 用来定义,可有多个超类 Sup,中间用 , 分割。
    • 泛型约束和条件类型中的 extends 用来检测兼容性,即 Sup 是否兼容 Sub

类型与集合

既然是编程,下面从数学的角度来简单粗略地描述 TS 类型系统,(读者可略过,想深入的童鞋可移步:zhuanlan.zhihu.com/p/38081852)。

TS 中的类型好比数学中的集合,类型是具有某种特定性质的 JS 值的集合。 比如 number 类型对应 JS 中所有数值的集合。

类型集合的分类

  • any 类型对应为 全集
  • never 类型对应为 空集
  • 联合类型是类型集合之间的 并集
  • 交叉类型是类型集合之间的 交集

结语

本篇主要是对类型进行编程的能力进行了梳理。

协议

CC BY-NC-ND 4.0
LICENSE

本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。

《探索 TypeScript 类型注解》

  1. [x] 扩展的 JavaScript
  2. [x] 数据类型
  3. [x] 自定义类型
  4. [x] 类型检查
  5. [x] 高级类型
  6. [x] 类型编程

参考链接