Flow 交集类型(Intersection Types)完全指南:语法、函数重载、对象组合与不可满足类型

原创2026-09-21 11:04:101,737 阅读
文章标签:开发工具静态分析代码质量

Flow 交集类型(Intersection Types)完全指南:语法、函数重载、对象组合与不可满足类型

本文基于 Flow 官方类型系统文档《Intersections》撰写,完整讲解交集类型的语法、求值语义("全入一出")、函数类型交集(重载)、对象类型交集与顺序敏感的合并规则,以及精确对象交集导致的"不可能类型"陷阱,并结合当前仓库源码(Rust 类型检查内核 rust_port/crates/flow_typing、官方测试集 tests/intersection)给出实现级佐证。读完本文你将掌握:用 & 声明重载函数、在 inexact 对象间做交集、用对象类型展开(object type spread)替代精确对象交集,并理解 Flow 为什么在对象组合上选择与 TypeScript 截然不同的路径。

交集类型(intersection type)表示一个值同时属于一组类型中的每一个,即"既满足 A,又满足 B,还满足 C"。它是并集类型(union,表示"满足其中任意一个")的对偶。在 Flow 中,交集类型由 & 运算符连接,最常见的两大用途是:为函数声明多个调用签名(重载),以及要求一个值同时实现多个 interface。

interface Serializable {
  serialize(): string;
}

interface HasLength {
  length: number;
}

function func(value: Serializable & HasLength) {
  // ...
}

func({
  length: 3,
  serialize() {
    return '3';
  },
}); // Works

func({length: 3}); // Error! Doesn't implement both interfaces

上面这个 func({length: 3}) 报错,正是因为 {length: 3} 只满足 HasLength,却不满足 Serializable——交集类型要求传入值同时满足所有成员。

与 TypeScript 的对照 TypeScript 习惯用交集(A & {b: T})合并对象类型,而 Flow 的习惯是类型层面的展开({...A, b: T})。Flow 的对象类型默认是精确的(exact),而精确对象之间做交集会得到一个不可能类型](#不可能交集类型-impossible-intersection-types);对不精确对象({a: number, ...})做交集则是受支持的。用于表达多个调用签名的函数重载交集,两个语言的行为则直接对应。完整对比见 [Object spread typing 的 Object 组合一节。

何时使用交集类型

  • 声明重载函数:用函数类型的交集表达"根据传入参数的不同返回不同类型",见函数类型的交集;
  • 要求一个值实现多个接口:例如 Serializable & HasLength,需要值同时满足多个 interface(见 interfaces 文档);
  • 组合不精确对象:{a: number, ...} & {b: boolean, ...} 可以得到同时含两组属性的对象。

而组合精确对象类型时,应当改用对象类型展开(object type spread)——因为精确对象相交会产生不可能类型。

交集类型语法

交集类型是由 & 连接任意多个类型:

Type1 & Type2 & ... & TypeN

也可以在最前面带一个前导 &,这在把长交集拆成多行时非常有用:

type Foo =
  & Type1
  & Type2
  & ...
  & TypeN

交集的每个成员可以是任意类型,甚至本身又是一个交集类型:

type Foo = Type1 & Type2;
type Bar = Type3 & Type4;

type Baz = Foo & Bar;

交集类型:全部入,一个出

交集类型与并集类型恰好相反。调用一个接受交集类型的函数时,传入的值必须同时满足全部成员类型(all in);但在函数内部,我们只需要把它当作其中任意一个类型来使用(one out)。

type A = {a: number, ...};
type B = {b: boolean, ...};
type C = {c: string, ...};

function func(value: A & B & C) {
  const a: A = value;
  const b: B = value;
  const c: C = value;
}

即使我们依次把 value 当作 A、B、C 中单独的一个来使用,也不会报错——因为它同时满足所有成员。这种"一次满足、处处可用"的语义,正是交集类型作为接口组合工具的根基。

函数类型的交集

交集类型最经典的用途是表达重载函数:根据传入参数的不同,函数返回不同类型。假设我们要为一个函数写类型,其行为是:

  • 传入 "string" 时返回 string;
  • 传入 "number" 时返回 number;
  • 传入其他任意字符串时返回任意可能的类型(unknown)。

这个函数的类型可以写作:

type Fn =
  & ((x: "string") => string)
  & ((x: "number") => number)
  & ((x: string) => unknown);

上面定义中的每一行称为一个 overload(重载),我们说 Fn 类型的函数是 overloaded(重载的)。

注意箭头类型周围的圆括号是必需的:它们用来覆盖"箭头构造符"高于交集运算符的优先级,若不写括号,(x: "string") => string & (...) 的解析结果将完全不同。

调用一个重载函数

基于上面的定义,可以声明一个行为如下的函数 fn:

declare const fn:
  & ((x: "string") => string)
  & ((x: "number") => number)
  & ((x: string) => unknown);

const s: string = fn("string"); // Works
const n: number = fn("number"); // Works
const b: boolean = fn("boolean"); // Error!

Flow 实现这一行为的方式是:把实参类型与第一个参数类型兼容的重载进行匹配。注意 "string" 同时匹配第一个和最后一个重载,Flow 只会取第一个。如果没有重载匹配,Flow 会在调用点报错。

声明重载函数

声明同一个 fn 的等价方式是连续使用多条 declare function 语句:

declare function fn(x: "string"): string;
declare function fn(x: "number"): number;
declare function fn(x: string): unknown;

Flow 的一个限制是:它无法针对交集类型检查函数体。换句话说,如果在上述声明之后紧接着给出如下实现:

function fn(x: unknown) {
  if (x === "string") { return ""; }
  else if (x === "number") { return 0; }
  else { return null; }
}

Flow 会静默接受它(并以 Fn 作为推断出的类型),但不会用这个签名去检查实现体。这意味着这类声明更适合放在库定义(libdefs)中——那里实现是被省略的。在仓库中,流式声明(declare function)与 declare const 等环境声明形式,正是 Flow 与 TypeScript 对照文档中列出的两语言直接对应(match)的语法形式之一。

对象类型的交集

当对不精确对象类型(inexact object types)取交集时,含义是:该对象满足交集中的每一个成员。

例如,对两组属性各不相同的不精确对象取交集,结果是同时拥有所有属性的对象:

type One = {foo: number, ...};
type Two = {bar: boolean, ...};

type Both = One & Two;

const value: Both = {
  foo: 1,
  bar: true
};

当属性名重叠时,Flow 采取与重载函数相同的策略:返回匹配该名字的第一个属性所属的类型。

例如,把两个都含 prop 属性的不精确对象合并——第一个 prop 类型为 number,第二个为 boolean——访问 prop 会得到 number:

type One = {prop: number, ...};
type Two = {prop: boolean, ...};

declare const both: One & Two;

const prop1: number = both.prop; // Works
const prop2: boolean = both.prop; // Error!

这一行为与仓库中的测试互相印证:在 tests/intersection/lib/lib.js 中定义了 type ObjAssignT = { foo: string, ... } & { bar: string, ... }; 等对象交集类型,而 tests/intersection/objassign.js 则验证"交集的对象的展开赋值"(let y: ObjAssignT = { ...x };)是允许的;tests/intersection/intersection.js 直接测试了 Error & {type: number} 的交叉类型可以正常读取其成员属性 x.type。

要组合精确对象类型,应该改用对象类型展开(object type spread):

type One = {foo: number};
type Two = {bar: boolean};

type Both = {
  ...One,
  ...Two,
};

const value: Both = {
  foo: 1,
  bar: true
};

注意:就对象而言,Flow 中交集类型"按顺序"的实现方式,从集合论角度常常显得反直觉——在集合里交集的运算对象可以任意交换顺序(满足交换律)。因此,更好的实践是用顺序语义更明确的对象类型展开来定义这类操作。这一点在 objects 文档中有更完整的展开:对象类型展开直接镜像了运行时值展开的语义——只含自有属性(所以不能展开 interface,因为 interface 不跟踪属性是否为"own")、后者覆盖前者、精确性会传播。

从实现层面看,Flow 的 Rust 类型检查内核在 rust_port/crates/flow_typing/src/merge.rs 中以 IntersectionT 表示交集类型(TypeInner::IntersectionT(_, rep)),并在求值、查找属性键(find_key_prop)等环节逐一遍历成员;例如判断一个交集类型是否"可为 void"时要求所有成员都 voidable(rep.members_iter().all(...)),这与文档"交集要求全部满足"的语义完全一致。解析端则在 rust_port/crates/flow_parser/src/estree_translator.rs 生成 IntersectionTypeAnnotation 节点,& 也登记为解析器的标点符号(见 rust_port/crates/flow_parser_wasm/src/bin/codegen.rs 中 ("IntersectionType", "&", "Punctuator"))。

不可能交集类型

利用交集类型,可以构造出运行时不可能存在的类型。交集允许你组合任意类型,哪怕它们互相冲突。

例如,可以创建 number 与 string 的交集:

type NumberAndString = number & string;

function func(value: NumberAndString) { /* ... */ }

func(3.14); // Error!
func('hi'); // Error!

你无法造出一个"既是数字又是字符串"的值,但可以写出它的类型。这类类型没有实际用处,它只是交集类型工作机制的副产品。

一种很容易意外制造不可能类型的方式,是对精确对象类型取交集。例如:

function func(obj: {a: number} & {b: string}) { /* ... */ }

func({a: 1}); // Error!
func({b: 'hi'}); // Error!
func({a: 1, b: 'hi'}); // Error!

一个对象不可能"恰好只有属性 a 且没有其他属性",同时"恰好只有属性 b 且没有其他属性"——所以三者全部报错。这也正是 flow-vs-typescript 文档 所解释的:因为 Flow 的精确对象禁止未列出的属性,两个精确对象交集会产生不可能类型——只要 A 与 B 有任何差异,一个值不可能同时"恰好是 A"且"恰好是 B"。因此 Flow 需要另一种操作来组合精确对象,即类型层面的展开 {...A, ...B}。

参见

登录后查看全文
flow