TS类型技巧(五):联合类型
TypeScript 对联合类型做了专门的处理,具有 Distributive 特性, 得到了写法上的简化。
效果: 联合类型 的每一个元素会分别参与 类型计算,结果再次合并为 联合类型
目的: 简化类型编程逻辑,不需要递归提取每个元素再处理
缺点: 带来了理解上的门槛
1. 条件类型
这种效果叫 分布式条件类型
type UppercaseA<Item extends string> =
Item extends 'a' ? Uppercase<Item> : Item;
// "b" | "c" | "A"
type Result = UppercaseA<'a' | 'b' | 'c'>
<Item extends string>
联合类型 需要每一个子类型 均能通过泛型约束,才算通过
体验其特点
// 下划线转驼峰
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>]
: [];
// 如果想对联合类型调用CamelCase,不需要递归
type CamelCaseUnion<Item extends string> = CamelCase<Item>
// "aAA" | "bBB"
type test = CamelCaseUnion<'a_a_a' | 'b_b_b'>
CamelCase<Item & string>
转string 供CamelCase使用
2. 如何判断联合类型
type IsUnion<A, B = A> = A extends A
? [B] extends [A]
? false
: true
: never
// true
type test3 = IsUnion<'a' | 'b'>
B = A
复制一个 A
A extends A
仅为了 触发分布式条件类型, 让A 变成单独的子类型
[B] extends [A]
避免触发B
的分布式条件类型,与 已经变成子类型的 A
比较
如果相等,就是不存在分发,就不是 联合类型,
如果不相等, 就是 联合类型
只有extends左边的联合类型会触发 分别单独传入计算
// A B 是同一个类型,属性a b却不同
type TestUnion<A, B = A> = A extends A ? { a: A, b: B} : never;
// {
// a: "a";
// b: "a" | "b" | "c";
// } | {
// a: "b";
// b: "a" | "b" | "c";
// } | {
// a: "c";
// b: "a" | "b" | "c";
// }
type TestUnionResult = TestUnion<'a' | 'b' | 'c'>;
3. 模板字符串类型
字符串模板类型 传入联合类型,会产生 展开及交叉组合
type ConcatStr<T extends string, U extends string> = `${T}-${U}`;
// "A-a" | "A-b" | "B-a" | "B-b"
type A = ConcatStr<'A' | 'B', 'a' | 'b'>;
4. 索引类型
数组转联合类型, 使用number下标访问
type test = ['aaa', 'bbb']
// "aaa" | "bbb"
type test2 = ['aaa', 'bbb'][number]
// "aaa" | "bbb"
type test3 = test[number]
type T = { a: number } | { b: string };
// number | string
type Keys = T['a' | 'b'];
5. 利用分发机制
对数组进行全组合(4 和 3 中的内容), 下面只需要传入数组
type test4<T extends string[]> = `__${T[number]}`
// "__aaa" | "__bbb"
type test5 = test4<['aaa', 'bbb']>
T extends string[]
和 T[number]
这两点是实现能传入数组变成联合类型的关键
联合类型的分发 代替 循环, 达到遍历子类型的效果
// 任意两个类型的全组合
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" | "C" | "BC" | "CB" | "AB" | "AC" | "ABC" | "ACB" | "BA" | "CA" | "BCA" | "CBA" | "BAC" | "CAB"
type test6 = AllCombinations<'A' | 'B' | 'C'>
A extends A
将A分发, 相当于循环遍历了A的每个子类型
接下来对每个 单A 都调用一次 Combination,循环+递归 完成全组合
7. boolean any never 的分发
type ActiveDistribute<T> = T extends true ? 1 : 2
// 1 | 2
type testActiveDistribute = ActiveDistribute<any>
// 1 | 2
type testActiveDistribute1 = ActiveDistribute<boolean>
// never,严格来说也是分布式条件类型,分别比较,分别返回never,结果还是never
type testActiveDistribute2 = ActiveDistribute<never>
8. 联合类型的最后一个类型
联合类型不能直接 infer 来取其中的某个类型, 必须通过特殊技巧
联合类型转元组
// [1, 2, 3]
type testUnionToTuple = UnionToTuple<1 | 2 | 3>
// 联合类型转交叉类型
type UnionToIntersection<U> =
(U extends U ? (x: U) => unknown : never) extends
(x: infer R) => unknown
? R // { [K in keyof R]: R[K] }
: never
// 联合类型转元组类型
type UnionToTuple<T> =
UnionToIntersection<
T extends any ? () => T : never
> extends () => infer ReturnType
? [...UnionToTuple<Exclude<T, ReturnType>>, ReturnType]
: [];
// [1, 2, 3]
type testUnionToTuple = UnionToTuple<1 | 2 | 3>
T extends any ? () => T : never
联合类型转 函数联合类型
UnionToIntersection<>
函数联合类型 转 函数交叉类型
函数交叉类型 即 函数重载(见TS基础8函数重载)
函数重载 extends () => infer ReturnType
获取函数重载的 ReturnType
函数重载的ReturnType 特性是: 其值为最后一个重载的ReturnType
至此我们拿到了联合类型的最后一个类型!!
9. 其他
keyof T | keyof U
不能写成 keyof (T | U)
,这样是拿交集的Key
keyof 是一种内置操作符, 联合类型不对 keyof 分发