联合类型在类型编程中比较特殊,TypeScript 对它做了专门的处理,写法上可以简化,但也增加一些认知成本。
分布式条件类型
当类型参数为联合类型,并且在条件类型左边直接引用该类型参数的时候,TypeScript 会把每一个元素单独传入来做类型运算,最后再合并成联合类型,这个种语法叫做分布式条伯类型。
type Union = 'a' | 'b' | 'c';
type str = `${Union}~~`;
// type str = "a~~" | "b~~" | "c~~"
这样简化类型编程逻辑,不需要递归提取每个元素再处理。
联合类型的每个元素都是互不相关,不像数组、索引、字符串这些元素之间是有关系。设计成每一个独立处理,最后合并。
// Camelcase
type Camelcase<Str extends string> =
Str extends `${infer Left}_${infer Right}${infer Rest}`
? `${Left}${Uppercase<Right>}${Camelcase<Rest>}`
: Str;
// CamelcaseArr
type CamelcaseArr<
Arr extends unknown[],
Result extends unknown[] = []
> = Arr extends [infer Item, ...infer RestArr]
? [...Result, Camcelcase<Item & string>, ...CamelcaseArr<RestArr>]
: Result;
// CamelcaseUnion
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;
这段逻辑就是分布式条件类型带来的认知成本
type TestUnion<A, B = A> = A extends A ? { a: A, b: B } : never;
A 和 B 都是同一个联合,但是值不一样。因为条件类型中如果左边的类型是联合类型,会把每个元素单独传入做计算,而右边不会。
所以 A 是 ‘a’ ,B 是 ‘a’ | ‘b’ | ‘c’,A 是 ‘b’,B 是 ‘a’ | ‘b’ | ‘c’ 等等。
所以 IsUnion 中,类型参数 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 左边直接是类型参数才会触发发布式条件类型。
数组转联合类型
通过索引访问来变为联合类型
type union = ['aaa', 'bbb'][number];
// type union = "aaa" | "bbb";
// BEM CSS 命名规范,用 block__element--modifier 描述某个区块下面的某个元素的某个状态的样式
// 如果传入是数组,要递归遍历取出每一个元素来和其它部分组合处理
type BEM<
Block extends string,
Element extends string[],
Modifier extends string[]
> = `${Block}__${Element[number]}--${Modifier[number]}`;
全组合
// AllCombinations 全组合 'A'|'B' -> 'A'|'B'|'AB'|'BA'
type Combination<A extends string, B extends string> =
| A
| B
| `${A}${B}`
| `${B}${A}`;
// 任何两个类型的组合都有四种 A、B、AB、BA
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以后的所有组合就是 Combination<Exclude<B, A>>