关于 control-union-distribution
面向使用条件类型和联合类型的 TypeScript 开发者的技能。它说明条件类型何时分配,以及如何阻止或强制分配,并解释 boolean 和 never 的特殊结果。
主要功能
- 01解释条件类型的分配行为
- 02用单元素元组阻止分配
- 03用裸条件强制分配
- 04讲解 boolean 与 never 的边界情况
- 05指导使用联合输入进行测试
适用场景
- 01排查条件类型结果异常
- 02控制类型定义中的联合行为
- 03调试递归泛型类型
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill control-union-distribution安装后在 Claude Code 中输入/control-union-distribution确认已加载。
详细介绍
控制条件类型中联合类型的分配
概述
TypeScript 中的条件类型默认会对联合类型进行分配。这通常正是你想要的,但有时也会导致令人意外的行为。理解如何控制分配——既能在不需要时阻止分配,也能在需要时启用分配——是进行高级类型级编程的关键。
令人意外的情况包括:boolean 被视为 true | false,never 分配后得到 never,以及无法正确分配的递归类型。本技能将介绍如何处理这些情况。
何时使用此技能
- 条件类型在接收联合类型输入时表现异常
boolean类型产生令人意外的结果never类型的求值结果异常- 需要阻止联合类型的分配
- 递归泛型类型无法正确分配
铁律
将条件包装在单元素元组 [T] 中以阻止分配;添加裸条件 N extends... 以强制分配。理解 boolean 和 never 在可分配条件类型中的行为。
检测
留意以下令人意外的行为:
// Surprising: boolean distributes
type Celebrate<V> = V extends true ? 'Huzzah!' : never;
type Surprise = Celebrate<boolean>; // "Huzzah!" not never
// Surprising: never distributes to never
type AllowIn<T> = T extends { password: string } ? 'Yes' : 'No';
type N = AllowIn<never>; // never, not 'Yes' | 'No'
// Problem: recursive type doesn't distribute
type NTuple<T, N> = /* ... */; // NTuple<string, 2 | 3> gives wrong result
阻止分配
将条件包装在单元素元组 [T] 中:
// Problem: distributes over unions
type Comparable<T> =
T extends Date ? Date | number :
T extends number ? number :
T extends string ? string :
never;
// Date | string becomes (Date | number) | string - wrong!
let dateOrStr: Date | string;
const result: Comparable<typeof dateOrStr>; // Should be never
// Solution: wrap in one-tuple
type Comparable<T> =
[T] extends [Date] ? Date | number :
[T] extends [number] ? number :
[T] extends [string] ? string :
never;
// Now Date | string correctly evaluates to never
boolean 的意外行为
TypeScript 将 boolean 视为 true | false:
type CelebrateIfTrue<V> = V extends true ? 'Huzzah!' : never;
// Surprising result
type Party = CelebrateIfTrue<true>; // "Huzzah!"
type NoParty = CelebrateIfTrue<false>; // never
type Surprise = CelebrateIfTrue<boolean>; // "Huzzah!" (!)
// Why? boolean distributes:
// CelebrateIfTrue<true | false>
// = CelebrateIfTrue<true> | CelebrateIfTrue<false>
// = "Huzzah!" | never
// = "Huzzah!"
// Fix: prevent distribution
type CelebrateIfTrue<V> = [V] extends [true] ? 'Huzzah!' : never;
type SurpriseFixed = CelebrateIfTrue<boolean>; // never - correct!
never 的意外行为
never 被视为空联合类型:
type AllowIn<T> = T extends { password: string } ? 'Yes' : 'No';
// Surprising: never evaluates to never
type N = AllowIn<never>; // never (not 'Yes' or 'No')
// Why? never is empty union:
// AllowIn<never> = AllowIn<> = empty union = never
// Fix: wrap in one-tuple
type AllowIn<T> = [T] extends [{ password: string }] ? 'Yes' : 'No';
type NFixed = AllowIn<never>; // 'No' - correct!
启用分配
有时你需要强制进行分配。添加一个裸条件:
// Problem: recursive type doesn't distribute
type NTuple<T, N extends number> = NTupleHelp<T, N, []>;
type NTupleHelp<T, N, Acc extends T[]> =
Acc['length'] extends N
? Acc
: NTupleHelp<T, N, [T, ...Acc]>;
type PairOrTriple = NTuple<string, 2 | 3>;
// Got: [string, string] (wrong!)
// Want: [string, string] | [string, string, string]
// Solution: add distributive wrapper
type NTuple<T, N extends number> =
N extends number // Forces distribution
? NTupleHelp<T, N, []>
: never;
type PairOrTripleFixed = NTuple<string, 2 | 3>;
// Now: [string, string] | [string, string, string] - correct!
完整示例
// Type-safe comparison function
type Comparable<T> =
[T] extends [Date] ? Date | number : // Prevent distribution
[T] extends [number] ? number :
[T] extends [string] ? string :
never;
declare function isLessThan<T>(a: T, b: Comparable<T>): boolean;
// Valid comparisons
isLessThan(new Date(), new Date()); // OK
isLessThan(new Date(), Date.now()); // OK (Date/number)
isLessThan(12, 23); // OK
isLessThan('A', 'B'); // OK
// Invalid comparison - correctly rejected
isLessThan(12, 'B'); // Error: string not assignable to number
// Union case - correctly rejected
let dateOrStr: Date | string;
isLessThan(dateOrStr, 'B'); // Error: string not assignable to never
抗压协议
当条件类型表现异常时:
- 检查是否发生分配:类型是否在不该分配时对联合类型进行了分配?
- 使用
boolean/never测试:它们通常能暴露分配问题 - 包装在单元素元组中:
[T] extends [X]可阻止分配 - 添加裸条件:
N extends any可强制分配 - 使用联合类型验证:用联合类型输入测试你的类型
危险信号
| 症状 | 原因 | 修复方法 |
|---|---|---|
boolean 得到意外结果 | 分配 | 包装在 [T] 中 |
never 得到 never | 空联合类型 | 包装在 [T] 中 |
| 联合类型未正确拆分 | 未进行分配 | 添加裸 N extends |
| 想要交集,却得到了联合类型 | 分配 | 包装在 [T] 中 |
常见借口
“复杂情况我直接用 any 就行”
事实:理解分配机制能让你精确控制类型。使用 any 会牺牲所有类型安全性。
“这对我的使用场景来说太复杂了”
事实:单元素元组技巧很简单:[T] extends [X] 与 T extends X 的区别。学会一次,终身受用。
“类型系统不应该这样工作”
事实:分配是一项强大的功能。理解它,就能驾驭这种能力,而不是与之对抗。
快速参考
| 目标 | 语法 | 示例 |
|---|---|---|
| 允许分配 | T extends X | 默认行为 |
| 阻止分配 | [T] extends [X] | 用于联合类型、boolean、never |
| 强制分配 | N extends any ? ... : never | 用于递归类型 |
总结
对联合类型进行分配通常正是你想要的,但并非总是如此。使用 [T] extends [X] 阻止分配,使用裸条件强制分配。理解 boolean 和 never 的行为,避免意外情况。
参考资料
- Dan Vanderkam 著《Effective TypeScript》第 2 版
- 条目 53:了解如何控制条件类型中联合类型的分配
评论
支持 Markdown