Jacleklm's Blog

TypeScript中的类型编程

2020/12/07

前言

本篇文章是读了林不渡的TypeScript 的另一面:类型编程,码了一些 demo 并查了其他文档等最后留下的学习笔记。建议直接读原文,本文可读性较差

类型编程的特点/看法

  • 它会带来代码量大大增多(可能接近甚至超过业务代码),编码耗时增长等问题,而带来的唯一好处就是类型安全,包括的类型提示,进一步减少可能存在的调用错误,以及降低维护成本。看起来似乎有得有失,但实际上,假设你花费 1 单位脑力使用基础的 TS 以及简单的类型编程,你就能够获得 5 个单位的回馈。但接下来,有可能你花费 10 个单位脑力,也只能再获得 2 个单位的回馈
  • 另外一个类型编程不受重视的原因则是实际业务中并不会需要多么苛刻的类型定义,通常是底层框架类库才会有此类需求

泛型 Generic Type

TypeScript 基础语法小结

在类型编程里,泛型就是变量

比如我们要写一个类似于 Pick 功能的函数,从一个 obj 中挑选一些键值对出来,可以这么写:

1
2
3
export function getValueListOfObj<T extends object, U extends keyof T>(obj: T, keys: U[]): T[U][] {
return keys.map((key: U) => obj[key]);
}

这里还用 extends 做了一定的泛型约束

索引类型与映射类型

索引类型

索引类型见TypeScript 基础语法小结

映射类型

映射类型 Mapped Types 通常用于在旧有类型的基础上进行改造,包括接口包含字段、字段的类型、修饰符(readonly 与?)等等。可以利用 Readonly,Partial 等这些 TS 内置工具类型(也叫类型接口)实现这个过程。

简单地说就是 TS 允许将一个类型映射成另外一个类型

一个很实用的实现就是 clone,把 T 里面的都拷贝一次

1
2
3
type cloneT<T> = {
[K in keyof T]: T[K];
};

上面这种已经算是工具类型的实现了。相当于类型编程中的 utils

这部分详见 TypeScript 中的内置工具类型及其实现

条件类型 Conditional Types

条件类型的语法实际上就是三元表达式。其中一个常见应用就是用来实现更精准的泛型约束,使得泛型收窄

1
T extends U ? X : Y

这种场景下 T 一般是联合类型。如果你觉得这里的 extends 不太好理解,可以暂时简单理解为 U 中的属性在 T 中都有

条件类型理解起来更直观,唯一需要有一定理解成本的就是何时条件类型系统会收集到足够的信息来确定类型,也就是说,条件类型有可能不会被立刻完成判断。
有时候,条件类型的推导会被延迟(deferred),因为此时类型系统没有足够的信息来完成判断:

1
2
// 单纯声明而已
declare function strOrNum<T extends boolean>(input: T): T extends true ? string : number;

只有给出了所需信息(在这里是input值),才可以完成推导

1
const strReturnType = strOrNum(true);

嵌套的条件类型

此外,就像三元表达式可以嵌套,条件类型也可以嵌套,如果你看过一些框架源码,也会发现其中存在着许多嵌套的条件类型,条件类型可以将类型约束收拢到非常精确的范围内

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type TypeName<T> = T extends string
? 'string'
: T extends number
? 'number'
: T extends boolean
? 'boolean'
: T extends undefined
? 'undefined'
: T extends Function
? 'function'
: 'object';

const good: TypeName<number> = 'number';
// Error
const bad: TypeName<string> = 'number';

分布式条件类型 Distributive Conditional Types

概念

分布式条件类型实际上不是一种特殊的条件类型,而是其特性之一。概括地说,就是对于属于裸类型参数的检查类型,条件类型会在实例化时期自动分发到联合类型上

原文: Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation

不负责任可能不太准确的简介

对这样一个场景:

1
type someTypeName<T> = T extends U ? X : Y

T为联合类型且T不被包裹,则T中的项会被拆开分别做extends U判断,再把结果合并组成一个联合类型作为推断结果

抽象起来就是:

1
2
3
( A | B | C ) extends U ? X : Y
// 相当于
(A extends U ? X : Y) | (B extends U ? X : Y) | (B extends U ? X : Y)

正确版的理清概念

先提取几个关键词,然后我们再通过例子理清这个概念:

  • 裸类型参数
  • 实例化
  • 分发到联合类型
1
2
3
4
5
6
7
8
9
10
// 使用上面的TypeName类型别名

// "string" | "function"
type T1 = TypeName<string | (() => void)>

// "string" | "object"
type T2 = TypeName<string | string[]>

// "object"
type T3 = TypeName<string[] | number[]>

我们发现在上面的例子里,条件类型的推导结果都是联合类型(T3实际上也是,只不过相同所以被合并了),并且就是类型参数被依次进行条件判断的结果。
但是当我们这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Naked<T> = T extends boolean ? "Y" : "N";
type Wrapped<T> = [T] extends [boolean] ? "Y" : "N";

/*
* 先分发到 Naked<number> | Naked<boolean>
* 结果是 "N" | "Y"
*/
type Distributed = Naked<number | boolean>;

/*
* 不会分发 直接是 [number | boolean] extends [boolean]
* 然后结果是 "N"
* 在这两个例子中,T 都是 number | boolean。区别是 T 有没有被【包裹】过
*/
type NotDistributed = Wrapped<number | boolean>;

现在我们可以来讲讲这几个概念了:

  • 裸类型参数,没有额外被接口/类型别名包裹过的,就像被Wrapped包裹后就不能再被称为裸类型参数。
  • 实例化,其实就是条件类型的判断过程,在这里两个例子的实例化过程实际上是不同的,具体会在下一点中介绍。
  • 分发至联合类型的过程
    对于TypeName,它内部的类型参数T是没有被包裹过的,所以
    TypeName<string | (() => void)> 会被分发为 TypeName<string> | TypeName<(() => void)>, 然后再次进行判断,最后分发为"string" | "function"
    抽象下具体过程:
    1
    2
    3
    ( A | B | C ) extends T ? X : Y
    // 相当于
    (A extends T ? X : Y) | (B extends T ? X : Y) | (B extends T ? X : Y)

一句话概括:没有被额外包装的联合类型参数T,在条件类型进行判定时会将联合类型分发,分别进行判断。

infer关键字

infer是inference的缩写,通常的使用方式是infer RR表示 待推断的类型。通常infer不会被直接使用,而是被放置在底层工具类型中,需要在条件类型中使用
看一个简单的例子,用于获取函数返回值类型的工具类型ReturnType,其用法:

1
2
const getStr = (): string => 'hello';
type fn = ReturnType<typeof getStr>; // 推断结果是 'string'

ReturnType源码如下

1
2
3
4
5
type ReturnType<T> = T extends (
...args: any[]
) => infer R
? R
: any;

这里的infer R就是声明一个变量来承载传入函数签名的返回值类型, 简单说就是用它取到函数返回值的类型方便之后使用。相当于预留一个变量来存函数的返回类型
类似前端中的loading占位符,infer也是这个思路,类型系统在获得足够的信息后,就能将infer后跟随的类型参数推导出来,最后返回这个推导结果

类型守卫(类型保护)Type Guards && is、in 关键字

TypeScript 基础语法小结

工具类型Tool Type 及其实现

工具类型就像我们自己的util或者我们用的lodash一样,虽然即使你还是不太懂这些工具类型的底层实现,也不影响你把它用好,不够我们自己还是得了解下比较好。推荐在完成学习后记录你觉得比较有价值的工具类型,并在自己的项目里新建一个.d.ts文件存储它

内置工具类型

TypeScript中的内置工具类型及其实现

社区工具类型

参考资料
TypeScript 的另一面:类型编程
深入理解 TypeScript
组内分享: TS 最佳实践

CATALOG
  1. 1. 前言
    1. 1.1. 类型编程的特点/看法
  2. 2. 泛型 Generic Type
  3. 3. 索引类型与映射类型
    1. 3.1. 索引类型
    2. 3.2. 映射类型
  4. 4. 条件类型 Conditional Types
    1. 4.1. 嵌套的条件类型
    2. 4.2. 分布式条件类型 Distributive Conditional Types
      1. 4.2.1. 概念
      2. 4.2.2. 不负责任可能不太准确的简介
      3. 4.2.3. 正确版的理清概念
  5. 5. infer关键字
  6. 6. 类型守卫(类型保护)Type Guards && is、in 关键字
  7. 7. 工具类型Tool Type 及其实现
    1. 7.1. 内置工具类型
    2. 7.2. 社区工具类型