mcpField
C
control-union-distribution

control-union-distribution

github.com/marius-townhouse开发642 次安装38 周前更新
代码仓库

关于 control-union-distribution

面向使用条件类型和联合类型的 TypeScript 开发者的技能。它说明条件类型何时分配,以及如何阻止或强制分配,并解释 boolean 和 never 的特殊结果。

主要功能

  1. 01解释条件类型的分配行为
  2. 02用单元素元组阻止分配
  3. 03用裸条件强制分配
  4. 04讲解 boolean 与 never 的边界情况
  5. 05指导使用联合输入进行测试

适用场景

  1. 01排查条件类型结果异常
  2. 02控制类型定义中的联合行为
  3. 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

抗压协议

当条件类型表现异常时:

  1. 检查是否发生分配:类型是否在不该分配时对联合类型进行了分配?
  2. 使用 boolean/never 测试:它们通常能暴露分配问题
  3. 包装在单元素元组中:[T] extends [X] 可阻止分配
  4. 添加裸条件:N extends any 可强制分配
  5. 使用联合类型验证:用联合类型输入测试你的类型

危险信号

症状原因修复方法
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