类型体操是TypeScript社区中一种特殊的编程练习,它要求开发者仅借助类型系统本身,在不写任何运行时代码的前提下完成逻辑运算、数据处理甚至算法实现。其中联合类型的处理是最经典也最容易踩坑的一环:当你对一个联合类型做条件判断时,结果往往会出乎意料地被“拆开”计算。本文将围绕如何在TypeScript中对联合类型的所有成员执行按位运算与逻辑运算展开,从原理到实战完整拆解。

理解分布式条件类型:联合类型运算的基石
在TypeScript中,当条件类型的检查对象是一个泛型参数且传入的是联合类型时,编译器会把联合类型自动拆开,对每一个成员分别执行条件判断,最后把结果重新组合成联合类型。这个行为叫分布式条件类型(Distributive Conditional Types)。看一个最简单的例子:
type IsString<T> = T extends string ? true : false; // 分布式生效:对 string 和 number 分别判断 type R1 = IsString<string | number>; // true | false // 用元组包裹后分布性被阻止 type IsString2<T> = [T] extends [string] ? true : false; type R2 = IsString2<string | number>; // false
这两个写法的差异是所有联合类型类型体操的核心。第一种写法中,T extends string会被拆解为string extends string和number extends string两次判断,得到true | false。第二种写法用元组[T]包裹,破坏了分布性,整个联合类型被当成一个整体去判断,所以结果直接是false。
什么时候希望分布、什么时候不希望分布,取决于你的目标:如果你要对每个成员逐个做逻辑运算(比如过滤出所有字符串成员),分布性是朋友;如果你要判断整个联合类型的整体性质(比如所有成员都是字符串),就必须阻止分布或者自己递归处理。记住这个分界线,后面的所有技巧都建立在它之上。
对联合类型所有成员执行逻辑运算
实现All语义:判断所有成员是否都满足条件
“所有成员都满足条件”是一个典型的全称量词逻辑。直接写T extends string ? true : false是不行的,因为分布性会把它变成逐个判断,只要有一个成员满足就会得到true这个结果项。正确的思路有两种。第一种是利用一个巧妙的恒等式:如果T extends CheckCondition成立,说明所有成员都满足;再反向判断CheckCondition extends T,如果也成立,说明成员集合完全等于条件集合。
type AllString<T> = T extends string ? true : false; // 方法一:阻止分布,整体判断 type AllStringV2<T> = [T] extends [string] ? true : false; // 方法二:双重判断,且要求至少有一个成员 type StrictAllString<T> = [T] extends [never] ? false : [string] extends [T] ? ([T] extends [string] ? true : false) : false; type A1 = StrictAllString<"a" | "b">; // true,两个成员都是 string 的子类型 type A2 = StrictAllString<"a" | 1>; // false,混入了 number
第二种思路是利用UnionToIntersection把联合类型转成交叉类型再判断。其原理是利用函数参数位置的逆变性:
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; // 交叉之后如果 extends string,说明每个成员都是 string type AllStringV3<T> = UnionToIntersection<T extends any ? T : never> extends string ? true : false; type A3 = AllStringV3<"a" | "b">; // true type A4 = AllStringV3<"a" | 1>; // false
注意UnionToIntersection内部先套了一层U extends any ? ... : never,这是为了强制触发分布性,把每个成员各自包成一个函数参数,然后再通过infer提取时,多个重载参数会收敛为交叉类型。这个技巧在判断“所有成员”类逻辑时几乎是必用的。
实现Any语义与成员过滤
“存在至少一个成员满足条件”的判断相对简单,分布性本身就帮我们完成了逐个判断,只需要看结果联合中是否包含true。而过滤成员则直接利用分布性加never的吸收特性——never在联合类型中会被自动丢弃:
type ExtractStrings<T> = T extends string ? T : never; type Filtered = ExtractStrings<"a" | 1 | "b" | 2>; // "a" | "b" type HasNumber<T> = [T extends number ? true : false] extends [false] ? false : true; type H1 = HasNumber<"a" | 1 | "b">; // true
这里HasNumber把分布的结果用元组包裹后再整体判断是否全为false,巧妙绕开了分布性对结果的“污染”。可以看到,逻辑运算的关键就是主动控制分布的发生与否,元组包裹和never吸收是两件最顺手的工具。
对联合类型所有成员执行按位运算
用模板字符串类型模拟按位运算
TypeScript的类型系统没有直接的按位与或非运算符,但数字可以转成模板字符串,字符串又可以逐位处理。经典的做法是把数字联合类型的每个成员当作标志位,通过映射和递归生成对应的位标记。下面实现一个把联合类型每个成员映射为2的幂次方标志位的工具:
// 数字转字符串再逐位构造,这里演示简化版:给每个成员分配一个位
type BitFlag<T extends PropertyKey> = T extends
"read" ? 1 :
"write" ? 2 :
"execute" ? 4 :
never;
type Permissions = "read" | "write" | "execute";
// 对所有成员按位求或:1 | 2 | 4 = 7
type OrAll<T extends PropertyKey, Acc extends number = 0> =
T extends PropertyKey ? (Acc | BitFlag<T>) extends infer R
? R extends number ? number : never
: never
: never;
type AllPerm = OrAll<Permissions>; // number(编译期可推断 1|2|4 的并集语义)更严格的按位运算需要真正实现编译期二进制加法。其思路是:用模板字符串把数字拆成字符,用infer递归截取最后一位,模拟竖式加法并处理进位:
// 简化的编译期按位与(针对 "0" | "1" 组成的字符串)
type BitAnd<A extends string, B extends string> =
A extends "1" ? (B extends "1" ? "1" : "0") : "0";
type AndAll<T extends string, U extends string> =
T extends `${infer AH}${infer AT}`
? U extends `${infer BH}${infer BT}`
? `${BitAnd<AH, BH>}${AndAll<AT, BT>}`
: ""
: "";
type Bits = AndAll<"110", "101">; // "100"这个例子虽然只处理了等长二进制字符串,但已经体现了按位运算在类型层的完整实现路径:模板字符串拆位、递归处理、结果拼接。真实项目中的权限标志位场景,往往把成员枚举先映射成固定宽度的二进制串,再套用这套递归结构。
递归遍历联合成员的通用模式
联合类型本身是无序的,无法直接“遍历”,但可以借助元组做中转:先用分布性把成员推入元组,再递归处理元组。这是处理复杂按位运算的通用骨架:
type Push<Arr extends unknown[], V> = [...Arr, V];
type ToTuple<T, Result extends unknown[] = []> =
T extends any ? Push<Result, T> : never;
type Process<T extends unknown[]> =
T extends [infer F, ...infer Rest]
? F extends number
? [F, ...Process<Rest>]
: Process<Rest>
: [];
type NumOnly = Process<ToTuple<"a" | 1 | "b" | 2>>; // [1, 2] 的有序形式这种“联合转元组、元组递归、结果再重组”的三段式结构,可以覆盖绝大多数需要对所有成员做复杂运算的场景。它的代价是递归深度受编译器限制(默认大约50层递归),成员特别多的联合类型可能需要分段处理或放宽tsconfig中的递归上限。
实战中的注意事项与性能权衡
类型体操写起来爽,但也要注意编译性能与可维护性。第一,递归类型和深层条件判断会显著拖慢编译速度,一个复杂工具类型被多处引用时,TypeScript的缓存机制可以缓解,但单次实例化的开销依然存在。建议把复杂的运算结果用type别名固化下来,避免在接口定义中反复内联。
第二,分布性带来的隐式行为是排查类型错误时的高频来源。如果发现判断结果是一个联合而不是单值,第一反应应该是检查是否被分布了,用元组包裹[T]是标准止血手段。反过来,如果发现联合类型整体判断不生效,就要确认泛型是否裸露使用以触发分布。
第三,按位运算在类型层终究是模拟出来的,TypeScript的类型系统并不保证数字字面量运算的精确性,复杂场景下结果往往会退化为宽泛的number。如果你的业务确实需要精确的编译期数值计算(比如构建配置校验),可以考虑在构建脚本中用普通代码做计算,再把结果以.d.ts声明文件的形式注入类型层,这是工程上更稳妥的折中方案。
掌握联合类型的分布机制,就像掌握了类型体操的任督二脉。逻辑运算靠分布性逐个击破,全称判断靠交叉类型收拢,按位运算靠模板字符串与递归模拟——三者组合起来,你就能在编译期表达出相当复杂的业务约束,让错误在编辑器里就无所遁形。
TypeScript联合类型类型体操修改时间:2026-09-07 05:32:47