首页 / 文章 / 2026年的Flow与TypeScript:语法重叠与更严格的检查

2026年的Flow与TypeScript:语法重叠与更严格的检查

了解 Flow 的语法如何与 TypeScript 相似,以及它独特的匹配表达式、React 特有的类型,还有 TypeScript 漏过而 Flow 能检测到的运行时错误。

1178 词

到2026年,Flow已在三个重要方面取得发展:

  • 其语法现已与TypeScript高度相似:熟悉TypeScript的开发者能够理解Flow的大部分功能。
  • 它具备TypeScript尚未拥有的某些特性:最显著的是match表达式,以及专为React设计的componenthookrenders结构。
  • 当两种类型系统出现分歧时,Flow倾向于选择更严格的规则:它会标记出TypeScript允许但可能在运行时引发问题、悄悄破坏数据或引入细微逻辑错误的模式。

Flow与TypeScript的语法已趋于一致

看看下面的代码片段——你能分辨出这是Flow还是TypeScript吗?很可能无法区分。

type User = {
  readonly name: string,
  readonly age: number,
  readonly metadata: unknown,
};

function get<K extends keyof User>(user: User, key: K): User[K] {
  return user[key];
}

declare const user: User;
const age: number = get(user, 'age');

熟悉 TypeScript 的人会发现这里并无什么令人惊讶之处。该示例使用了 keyofreadonly 字段、unknown 类型、通过 T[K] 进行的索引访问,以及借助 extends 的限定泛型。Flow 还支持条件类型、映射类型、类型守卫和 as const,其功能与 TypeScript 非常相似。

本文的其余部分将重点介绍 Flow 相较于 TypeScript 在哪些方面有进一步的发展。

仅存在于 Flow 中的功能

以下是 Flow 具备而 TypeScript 不具备的功能。

match 表达式与语句

Flow 提供了作为表达式的 match 结构用于模式匹配。编译器会对其进行全面检查,并允许你在匹配过程中直接解构值。如果你忘记处理某个情况——比如 type: 'remove'——match 会指出问题并明确告诉你缺少了什么。

type Action =
  | {type: 'add', text: string}
  | {type: 'toggle', id: string}
  | {type: 'remove', id: string};

declare const action: Action;

const description = match (action) { // ERROR: 'remove' case missing
  {type: 'add', const text} => `Add: ${text}`,
  {type: 'toggle', const id} => `Toggle ${id}`,
};

此外,match 还有语句形式,其行为类似于无法跳转的 switch,并且具备与表达式版本相同的所有功能。

React:componenthookrenders

  • component 语法:将 React 组件视为语言级的内置概念。属性直接作为命名参数声明,类型检查器可以强制执行 React 特有的正确性规则。
  • renders 类型:允许你描述组件之间如何组合。设计系统与组件库可以明确指定插槽允许接收什么内容以及组件允许输出什么内容,检查器甚至能通过包装组件来强制执行这些规则。
  • hook 语法:将钩子标记为一种独立于普通函数的类型。Flow 将React 规则直接集成到其类型检查器中,能够检测条件性的钩子调用、将钩子与普通函数混用,以及在渲染过程中对钩子返回值进行不安全的修改——所有这些都不需要额外的 ESLint 插件。
  • component Header(text: string, color: string) {
      return <div style={{color}}>{text}</div>;
    }
    component MainHeader(text: string) renders Header {
      return <Header text={text} color="red" />;
    }
    
    component Layout(header: renders Header) {
      return <div>
        {header}
        <section>Content</section>
      </div>;
    }
    
    const ok = <Layout header={<MainHeader text="Flow" />} />;
    const bad = <Layout header={<footer />} />; // ERROR
    

    TypeScript 捕获不到的四种运行时崩溃 —— 但 Flow 能

    这就是两种类型系统真正出现差异的地方。下面的所有示例在开启 strict 模式的 TypeScript 6.0.3 环境下都能通过类型检查,但在运行时仍然会失败。

    1. 在 TypeScript 中,从实例中提取方法会使其 this 绑定丢失
    class Counter {
      count: number = 0;
      increment(): number {
        return ++this.count;
      }
    }
    const counter = new Counter();
    const tick = counter.increment;  // TS accepts. Flow rejects.
    tick();  // Runtime crash! `this` is undefined inside `increment`
    

    一旦将 counter.incrementcounter 对象中提取出来,它就不再与该对象绑定——因此之后以 tick() 的形式调用时,this 的值为 undefined,从而导致 ++this.count 抛出异常。TypeScript 将被提取的方法视为普通函数,不会对此提出任何警告就允许其调用。而 Flow 则会在 this 绑定丢失的那一刻就拒绝这种提取操作。

    1. TypeScript 允许通过间接赋值让额外的属性悄悄进入
    type Prices = {apple: number, banana: number};
    const items = {apple: 1.5, banana: 0.5, sample: "free"};
    const prices: Prices = items;  // TS accepts. Flow rejects.
    Object.values(prices).map(
      price => price.toFixed(2), // Runtime crash! `sample` isn't a number
    );
    

    从技术上讲,TypeScript 的对象类型允许存在额外的属性。通常会标记像 {apple: 1.5, sample: "free"} 这样内容的“多余属性检查”,但只有在你直接赋值对象字面量时才会触发。如果通过中间变量传递相同的值,这项检查就不再适用——因此多余的 sample 字段就能未被发现地通过。而 Flow 的对象类型默认是严格的,意味着无论值以何种方式到达目标位置,额外的属性都会被拒绝。

    1. TypeScript 允许将更宽泛的类型放入更狭窄的可变数组中
    // TypeScript: accepted.
    function appendError(errs: Array<string | Error>) {
      errs.push(new Error("oops"));
    }
    const errors: Array<string> = [];
    appendError(errors);  // TS accepts. Flow rejects.
    errors[0].toUpperCase();  // Runtime crash! `errors[0]` isn't a string
    

    由于 TypeScript 将可变数组视为协变类型,因此它认为 Array<string>Array<string | Error> 的子类型。这就意味着该调用会被接受,而 appendError 函数中的 push 操作最终会将一个 Error 对象插入到调用方原本认为仅包含 string 类型元素的数组中。Flow 通过将可变数组视为不可变类型,在调用点就阻止了这种类型扩展,从而避免了这一问题。如果函数确实没有修改输入数据的必要,将参数改为 ReadonlyArray<string | Error> 即可完全消除风险,因为不可变性使得类型扩展不会带来任何问题。

    1. TypeScript 不会验证类型守卫体实际执行的内容
    // TypeScript: accepted, but this body is true for numbers, not strings.
    function isString(x: unknown): x is string {
      return typeof x === "number";  // TS accepts. Flow rejects.
    }
    const data: unknown = 1;
    if (isString(data)) {
      data.toUpperCase();  // Runtime crash! `data` isn't a string
    }
    

    TypeScript仅检查类型谓词所声明的签名,而从不查看函数体实际返回的值。Flow则会从两个方向进行验证:每个return语句都必须真正将结果限定为守卫条件所指定的类型,同时else分支也必须正确排除该类型。因此,如果谓词的逻辑与其声称要检查的内容不符,Flow就会拒绝它。

    阅读完整对比内容

    为了获得更多示例,Flow的官方文档网站提供了详尽的对比说明,逐点分析这两种语言的差异,涵盖了此处展示之外的二十多种情况:查看对比指南

    相关阅读