09-套路五:联合分散可简化
课程
1
如何阅读本小册
已学完
学习时长: 2分6秒
2
为什么说 TypeScript 的火爆是必然?
已学完
学习时长: 4分28秒
3
TypeScript 类型编程为什么被叫做类型体操?
已学完
学习时长: 9分16秒
4
TypeScript 类型系统支持哪些类型和类型运算?
已学完
学习时长: 19分27秒
5
套路一:模式匹配做提取
已学完
学习时长: 41分32秒
6
套路二:重新构造做变换
已学完
学习时长: 39分51秒
7
套路三:递归复用做循环
学习时长: 42分22秒
8
套路四:数组长度做计数
学习时长: 27分58秒
9
套路五:联合分散可简化
学习时长: 19分36秒
10
套路六:特殊特性要记清
学习时长: 36分41秒
11
类型体操顺口溜
学习时长: 27分26秒
12
TypeScript 内置的高级类型有哪些?
学习时长: 31分59秒
13
真实案例说明类型编程的意义
学习时长: 34分36秒
14
类型编程综合实战一
学习时长: 25分55秒
15
类型编程综合实战二
学习时长: 29分9秒
16
新语法 infer extends 是如何简化类型编程的
学习时长: 11分14秒
17
原理篇:逆变、协变、双向协变、不变
学习时长: 11分31秒
18
原理篇:编译 ts 代码用 tsc 还是 babel?
学习时长: 19分49秒
19
原理篇:实现简易 TypeScript 类型检查
学习时长: 47分51秒
20
原理篇:如何阅读 TypeScript 源码
学习时长: 19分27秒
21
原理篇:一些特殊情况的说明
已学完
学习时长: 12分39秒
22
小册总结
学习时长: 2分32秒
23
加餐:3 种类型来源和 3 种模块语法
学习时长: 17分6秒
24
加餐:用 Project Reference 优化 tsc 编译性能
已学完
学习时长: 4分47秒
juejin_logo copyCreated with Sketch.

联合类型在类型编程中是比较特殊的,TypeScript 对它做了专门的处理,写法上可以简化,但也增加了一些认知成本。

这是类型体操的第五个套路:联合分散可简化。

分布式条件类型

当类型参数为联合类型,并且在条件类型左边直接引用该类型参数的时候,TypeScript 会把每一个元素单独传入来做类型运算,最后再合并成联合类型,这种语法叫做分布式条件类型。

比如这样一个联合类型:

type Union = 'a' | 'b' | 'c';

我们想把其中的 a 大写,就可以这样写:

type UppercaseA<Item extends string> = 
    Item extends 'a' ?  Uppercase<Item> : Item;

可以看到,我们类型参数 Item 约束为 string,条件类型的判断中也是判断是否是 a,但传入的是联合类型。

这就是 TypeScript 对联合类型在条件类型中使用时的特殊处理:会把联合类型的每一个元素单独传入做类型计算,最后合并。

这和联合类型遇到字符串时的处理一样:

这样确实是简化了类型编程逻辑的,不需要递归提取每个元素再处理。

TypeScript 之所以这样处理联合类型也很容易理解,因为联合类型的每个元素都是互不相关的,不像数组、索引、字符串那样元素之间是有关系的。所以设计成了每一个单独处理,最后合并。

知道了 TypeScript 怎么处理的联合类型,趁热打铁来练习一下:

CamelcaseUnion

Camelcase 我们实现过,就是提取字符串中的字符,首字母大写以后重新构造一个新的。

type Camelcase<Str extends string> = 
    Str extends `${infer Left}_${infer Right}${infer Rest}`
    ? `${Left}${Uppercase<Right>}${Camelcase<Rest>}`
    : Str;

提取 _ 左右的字符,把右边字符大写之后构造成新的字符串,余下的字符串递归处理。

如果是对字符串数组做 Camelcase,那就要递归处理每一个元素:

type CamelcaseArr<
  Arr extends unknown[]
> = Arr extends [infer Item, ...infer RestArr]
  ? [Camelcase<Item & string>, ...CamelcaseArr<RestArr>]
  : [];

类型参数 Arr 为待处理数组。

递归提取每一个元素做 Camelcase,因为 Camelcase 要求传入 string,这里要 & string 来变成 string 类型。

那如果是联合类型呢?

联合类型不需要递归提取每个元素,TypeScript 内部会把每一个元素传入单独做计算,之后把每个元素的计算结果合并成联合类型。

type CamelcaseUnion<Item extends string> = 
  Item extends `${infer Left}_${infer Right}${infer Rest}` 
    ? `${Left}${Uppercase<Right>}${CamelcaseUnion<Rest>}` 
    : Item;

试一下

这不和单个字符串的处理没区别么?

没错,对联合类型的处理和对单个类型的处理没什么区别,TypeScript 会把每个单独的类型拆开传入。不需要像数组类型那样需要递归提取每个元素做处理。

确实简化了很多,好像都是优点?

也不全是,其实这样处理也增加了一些认知成本,不信我们再来看个例子:

IsUnion

判断联合类型我们会这样写:

type IsUnion<A, B = A> =
    A extends A
        ? [B] extends [A]
            ? false
            : true
        : never

当传入联合类型时,会返回 true:

当传入其他类型时,会返回 false:

试一下

是不是在心里会问:什么鬼?这段逻辑是啥?

这就是分布式条件类型带来的认知成本。

我们先来看这样一个类型:

type TestUnion<A, B = A> = A  extends A ? { a: A, b: B} : never;

type TestUnionResult = TestUnion<'a' | 'b' | 'c'>;

传入联合类型 'a' | 'b' | 'c' 的时候,结果是这样的:

A 和 B 都是同一个联合类型,为啥值还不一样呢?

因为条件类型中如果左边的类型是联合类型,会把每个元素单独传入做计算,而右边不会。

所以 A 是 'a' 的时候,B 是 'a' | 'b' | 'c', A 是 'b' 的时候,B 是 'a' | 'b' | 'c'。。。

试一下

那么利用这个特点就可以实现 Union 类型的判断:

type IsUnion<A, B = A> =
    A extends A
        ? [B] extends [A]
            ? false
            : true
        : never

类型参数 A、B 是待判断的联合类型,B 默认值为 A,也就是同一个类型。

A extends A 这段看似没啥意义,主要是为了触发分布式条件类型,让 A 的每个类型单独传入。

[B] extends [A] 这样不直接写 B 就可以避免触发分布式条件类型,那么 B 就是整个联合类型。

B 是联合类型整体,而 A 是单个类型,自然不成立,而其它类型没有这种特殊处理,A 和 B 都是同一个,怎么判断都成立。

利用这个特点就可以判断出是否是联合类型。

其中有两个点比较困惑,我们重点记一下:

当 A 是联合类型时:

  • A extends A 这种写法是为了触发分布式条件类型,让每个类型单独传入处理的,没别的意义。

  • A extends A 和 [A] extends [A] 是不同的处理,前者是单个类型和整个类型做判断,后者两边都是整个联合类型,因为只有 extends 左边直接是类型参数才会触发分布式条件类型。

理解了这两点,分布式条件类型就算掌握了。

掌握了难点之后,我们再做些练习:

BEM

bem 是 css 命名规范,用 block__element--modifier 的形式来描述某个区块下面的某个元素的某个状态的样式。

那么我们可以写这样一个高级类型,传入 block、element、modifier,返回构造出的 class 名:

这样使用:

type bemResult = BEM<'guang', ['aaa', 'bbb'], ['warning', 'success']>;

它的实现就是三部分的合并,但传入的是数组,要递归遍历取出每一个元素来和其他部分组合,这样太麻烦了。

而如果是联合类型就不用递归遍历了,因为联合类型遇到字符串也是会单独每个元素单独传入做处理。

数组转联合类型可以这样写:

那么 BEM 就可以这样实现:

type BEM<
    Block extends string,
    Element extends string[],
    Modifiers extends string[]
> = `${Block}__${Element[number]}--${Modifiers[number]}`;

类型参数 Block、Element、Modifiers 分别是 bem 规范的三部分,其中 Element 和 Modifiers 都可能多个,约束为 string[]。

构造一个字符串类型,其中 Element 和 Modifiers 通过索引访问来变为联合类型。

字符串类型中遇到联合类型的时候,会每个元素单独传入计算,也就是这样的效果:

试一下

可以看到,用好了联合类型,确实能简化类型编程逻辑。

AllCombinations

我们再来实现一个全组合的高级类型,也是联合类型相关的:

希望传入 'A' | 'B' 的时候,能够返回所有的组合: 'A' | 'B' | 'BA' | 'AB'。

这种全组合问题的实现思路就是两两组合,组合出的字符串再和其他字符串两两组和:

比如 'A' | 'B' | 'c',就是 A 和 B、C 组合,B 和 A、C 组合,C 和 A、B 组合。然后组合出来的字符串再和其他字符串组合。

任何两个类型的组合有四种:A、B、AB、BA

type Combination<A extends string, B extends string> =
    | A
    | B
    | `${A}${B}`
    | `${B}${A}`;

然后构造出来的字符串再和其他字符串组合。

所以全组合的高级类型就是这样:

type AllCombinations<A extends string, B extends string = A> = 
    A extends A
        ? Combination<A, AllCombinations<Exclude<B, A>>>
        : never;

类型参数 A、B 是待组合的两个联合类型,B 默认是 A 也就是同一个。

A extends A 的意义就是让联合类型每个类型单独传入做处理,上面我们刚学会。

A 的处理就是 A 和 B 中去掉 A 以后的所有类型组合,也就是 Combination<A, B 去掉 A 以后的所有组合>。

而 B 去掉 A 以后的所有组合就是 AllCombinations<Exclude<B, A>>,所以全组合就是 Combination<A, AllCombinations<Exclude<B, A>>>。

这里利用到了分布式条件类型的特性,通过 A extends A 来取出联合类型中的单个类型。

试一下

总结

联合类型中的每个类型都是相互独立的,TypeScript 对它做了特殊处理,也就是遇到字符串类型、条件类型的时候会把每个类型单独传入做计算,最后把每个类型的计算结果合并成联合类型。

条件类型左边是联合类型的时候就会触法这种处理,叫做分布式条件类型。

有两点特别要注意:

  • A extends A 不是没意义,意义是取出联合类型中的单个类型放入 A

  • A extends A 才是分布式条件类型, [A] extends [A] 就不是了,只有左边是单独的类型参数才可以。

我们后面做了一些案例,发现联合类型的这种 distributive 的特性确实能简化类型编程,但是也增加了认知成本,不过这也是不可避免的事。

本文案例的合并

留言
Ctrl + Enter
全部评论(31)
Asuka14024的头像
删除
前端工程师
看这章的话,把脑袋放空就看的懂了。[吃瓜群众]
点赞
回复
hedgehog_boy的头像
删除
晕,先快速跳过,后面再复看
1
回复
四棵呼呼的头像
删除
摸鱼带师
懂了,好像又没懂[捂脸]
点赞
回复
pass1on的头像
删除
type IsUnion<A, B = A> =
A extends A
? [B] extends [A]
? false
: true
: never

type R = IsUnion<'A' | 'B'>

type C1 = ('A' extends 'A'
? ['A' | 'B'] extends ['A']
? false
: true
: never)
| ('B' extends 'B'
? ['A' | 'B'] extends ['B']
? false
: true
: never)

type C2 = ('A' extends 'A' | 'B'
? ['A' | 'B'] extends ['A']
? false
: true
: never)
| ('B' extends 'A' | 'B'
? ['A' | 'B'] extends ['B']
? false
: true
: never)
展开
点赞
2
删除
R的计算过程应该是等于C1吧
点赞
回复
删除
没看明白 c1和R有什么区别
点赞
回复
用户4443592232210的头像
删除
AllCombinations妙啊
用分布式计算
Combination<"A", AllCombinations<B|C>>
Combination<"B", AllCombinations<A|C>>
Combination<"C", AllCombinations<A|B>>
再内联Combination<B,C>Combination<B,A>Combination<A,B>
最后全排列
展开
点赞
回复
自习是真的的头像
删除
前端开发
看晕了[流泪]
点赞
回复
三郎mr的头像
删除
1. 分布式条件类型,会每个元素单独传入计算;
2.字符串类型中遇到联合类型的时候,会每个元素单独传入计算;
明白了以上两点,再理解:
type Combination<A extends string, B extends string> =
| A
| B
| `${A}${B}`
| `${B}${A}`;

type test = Combination<'A', 'B' | 'C'>; //"A" | "B" | "C" | "AB" | "AC" | "BA" | "CA"

然后再去理解:
type AllCombinations<A extends string, B extends string = A> =
A extends A
? Combination<A, AllCombinations<Exclude<B, A>>>
: never;

type Combination 其实就是【2. 字符串类型中遇到联合类型的时候,会每个元素单独传入计算】的运用;

type AllCombinations 的主体逻辑其实就是 【1. 分布式条件类型,会每个元素单独传入计算;】的运用;

这样去理解 AllCombinations 就是豁然开朗了。

一开始 去理解AllCombinations 快一个小时,无法理解,然后从头再次阅读本节后,总结出上面两点,然后再动手试了下上面的 【type test //"A" | "B" | "C" | "AB" | "AC" | "BA" | "CA"】
才知道Combination 也是会触发单独计算, 才瞬间理解了type AllCombinations 。

此处建议 光神 补充下 【type test = Combination<'A', 'B' | 'C'>; //"A" | "B" | "C" | "AB" | "AC" | "BA" | "CA"】 这个例子,会给读者增加可理解性
展开
2
1
删除
谢谢,${union}点醒我了哈哈哈哈
点赞
回复
草苺奶昔的头像
删除
学生 @ 摸鱼
这个combination叫做permutation比较合适
点赞
回复
菜到_没救了的头像
删除
请问为什么以下text的类型始终是false,而不是 string | false?
type union1 = "union" | "age" | "name";

type text = union1 extends "union" ? string : false;
1
5
删除
你理解错了extends.
点赞
回复
删除
我已经清楚了,这个是分发条件类型的问题
你理解错了extends.
点赞
回复
查看更多回复
很帅很愁人的头像
删除
全村工程师 @ 下王庄村委员会
看晕了。
1
回复
钢蛋的头像
删除
前端开发工程师 @ 中关村某司
劝退章节
1
回复
爱吃鱼的桶哥Z的头像
删除
伪 · 全栈打杂攻城狮
这一章有点难,需要反复研读呀。
还有就是图中文字打错了。
点赞
1
删除
(作者)
我改改
点赞
回复
芹菜006的头像
删除
typeCamelcaseArr 这里的 Result 多余了吧。

这样写就可以了:
type CamelcaseArr<T extends unknown[]> = T extends [infer First, ...infer Rest]
? [CamelCase<First & string>, ...CamelcaseArr<Rest>]
: T;

或者这样:
type CamelcaseArr<
T extends unknown[],
Result extends string = ''
> = T extends [infer First, ...infer Rest]
? First extends `${infer F}_${infer S}${infer R}`
? [
...CamelcaseArr<[R], `${Result}${F}${Uppercase<S>}`>,
...CamelcaseArr<Rest>
]
: [`${Result}${First & string}`]
: T;
展开
1
1
删除
(作者)
都可以的,写法不唯一
点赞
回复
旋风冲锋的头像
删除
get到了一些,太草了
1
回复
瓦嘞嘞的头像
删除
type CamelCase<Str extends string> = Str extends `${infer Left}_${infer Right}${infer Rest}` ? `${Left}${Uppercase<Right>}${CamelCase<Rest>}` : Str; type A = CamelCase<'aa_aa' | 'bb_bb'> type X = ['aa_aa', 'bb_bb'] type Res = CamelCaseArr<X> type CamelCaseArr<Arr extends string[], ResStr extends string[] = []> = Arr extends [infer First, ...infer Rest] ? [...ResStr, CamelCase<First & string>, ...CamelCaseArr<Rest & string[]>] : ResStr
点赞
回复
小迷的头像
删除
前端
最后一个为什么要写B extends string = A,这个 = A是什么意思
点赞
1
删除
(作者)
默认值是参数 A
点赞
回复
YibuMe的头像
删除
Node.js探索者
type IsUnion<A, B = A> = A extends A // 这里 extends 左边的 A 触发分布式条件判断,右边的 A 是一个整体
? [B] extends [A] // 这里的 A 为什么是触发分布式条件判断 后 类型,是根据什么判断的?
? false
: true
: never;
点赞
1
删除
(作者)
因为 a 前面已经触发了啊,b 没触发过
点赞
回复
hehe1111的头像
删除
type union = ['aaa', 'bbb'][number] 为什么结果是 type union = 'aaa' | 'bbb'?此处能否多一些解释?[比心]
4
1
删除
(作者)
这个就是取索引值呀,数组类型取出所有的数字索引对应的值,然后组成联合类型
点赞
回复