看看这段代码,告诉我操作顺序:

const result = uppercase(trim(removeSpaces(validate(input))));

Enter fullscreen mode Exit fullscreen mode

你从左到右阅读,但它从右到左执行。validate 首先运行,然后是 removeSpaces,接着是 trim,最后是 uppercase。阅读顺序与执行顺序相反。

现在再添加一步:

const result = format(uppercase(trim(removeSpaces(validate(input)))));

Enter fullscreen mode Exit fullscreen mode

再添加一步:

const result = encode(format(uppercase(trim(removeSpaces(validate(input))))));

Enter fullscreen mode Exit fullscreen mode

这无法扩展。每个新步骤都将整个表达式包装在另一个函数调用中。嵌套不断增长。括号堆积如山。在中间插入步骤意味着要数括号。删除一个步骤也是如此。如果这些函数中的任何一个返回像 EitherTask 这样的包装类型,嵌套就会变得真正痛苦。

有一种更好的方法来编写这段代码。

pipe:从左到右的组合

pipe 将一个值作为第一个参数,并通过一系列函数从左到右传递它。每个函数接收前一个函数的返回值。

import { pipe } from "@oofp/core/pipe";

const result = pipe(input, validate, removeSpaces, trim, uppercase);

Enter fullscreen mode Exit fullscreen mode

相同的操作。相同的结果。但现在代码按执行顺序阅读:从 input 开始,验证它,删除空格,修剪它,大写它。

添加一个步骤很简单:

const result = pipe(input, validate, removeSpaces, trim, uppercase, encode);

Enter fullscreen mode Exit fullscreen mode

删除一个步骤同样容易——删除那一行。不需要数括号。不需要重构。

链式类型推断

TypeScript 推断每个中间类型。如果 validate 返回 string,那么 removeSpaces 必须接受 string。如果你传递一个不匹配的函数,编译器会立即告诉你:

const double = (n: number) => n * 2;
const exclaim = (s: string) => s + "!";
const len = (s: string) => s.length;

// TypeScript infers each step:
const result = pipe(
  "hello",        // string
  exclaim,        // (string) => string
  len,            // (string) => number
  double,         // (number) => number
);
// result: number (10)

Enter fullscreen mode Exit fullscreen mode

交换 doublelen,编译器会捕获它。类型通过管道流动,每个不匹配都会在编译时浮出水面。

垂直管道

当管道变长时,垂直书写。这是函数式 TypeScript 中的惯用风格:

import { pipe } from "@oofp/core/pipe";
import * as E from "@oofp/core/either";

const processInput = (raw: string) =>
  pipe(
    raw,
    parseJSON,
    E.chain(validateSchema),
    E.map(normalize),
    E.map(enrichWithDefaults),
    E.map(serialize),
  );

Enter fullscreen mode Exit fullscreen mode

每一行都是一个步骤。你可以从上到下扫描管道,一目了然地理解转换。

flow:创建可重用的管道

pipe 首先接受一个值并立即执行。有时你还没有这个值——你想定义管道并稍后应用它。那就是 flow

flow 只接受函数并返回一个新函数:

import { flow } from "@oofp/core/flow";

const process = flow(validate, removeSpaces, trim, uppercase);
// process is a function: (input: string) => string

const result = process(input);

Enter fullscreen mode Exit fullscreen mode

flow 从左到右组合,与 pipe 相同。不同之处在于计算发生的时间:pipe 现在运行,flow 为以后构建一个函数。

flow 的优势

当你需要将转换作为参数传递时使用 flow

import { flow } from "@oofp/core/flow";
import * as E from "@oofp/core/either";

const parsePositiveNumber = flow(
  (s: string) => Number(s),
  (n) => (isNaN(n) ? E.left("Not a number") : E.right(n)),
  E.chain((n) => (n > 0 ? E.right(n) : E.left("Must be positive"))),
);

// Use it directly with Array.map
const results = ["10", "-3", "abc", "42"].map(parsePositiveNumber);
// [Right(10), Left("Must be positive"), Left("Not a number"), Right(42)]

Enter fullscreen mode Exit fullscreen mode

因为 flow 返回一个普通函数,它可以与任何接受回调的 API 集成:Array.mapArray.filter、事件处理程序、中间件链等。

命名管道

flow 鼓励为转换命名。这是可读性的优势:

import { flow } from "@oofp/core/flow";
import * as M from "@oofp/core/maybe";

const parseAge = flow(
  M.fromNullable<string>,
  M.map((s) => parseInt(s, 10)),
  M.iif((n) => !isNaN(n) && n >= 0 && n <= 150),
);

const formatCurrency = flow(
  (cents: number) => cents / 100,
  (dollars) => dollars.toFixed(2),
  (s) => `$${s}`,
);

Enter fullscreen mode Exit fullscreen mode

现在 parseAgeformatCurrency 是自文档化的、可重用的,并且可以独立测试。

compose:从右到左(数学顺序)

compose 做的事情与 flow 相同,但顺序相反。函数按最外层优先列出,匹配数学符号 (f . g)(x) = f(g(x))

import { compose } from "@oofp/core/compose";

const process = compose(uppercase, trim, removeSpaces, validate);
// Same result as flow(validate, removeSpaces, trim, uppercase)

Enter fullscreen mode Exit fullscreen mode

从右到左阅读:validate,然后 remove spaces,然后 trim,然后 uppercase。

import { flow } from "@oofp/core/flow";
import { compose } from "@oofp/core/compose";

const exclaim = (s: string) => s + "!";
const upper = (s: string) => s.toUpperCase();
const trim = (s: string) => s.trim();

// These produce identical functions:
const withFlow = flow(trim, upper, exclaim);
const withCompose = compose(exclaim, upper, trim);

withFlow("  hi  ");    // "HI!"
withCompose("  hi  "); // "HI!"

Enter fullscreen mode Exit fullscreen mode

大多数 TypeScript 开发人员更喜欢 pipeflow,因为从左到右与我们阅读代码的方式一致。compose 存在是为了那些来自 Haskell 或范畴论、习惯于用 f . g 思考的人。使用对你的团队来说更易读的那个。

真实世界示例

数据验证管道

import { pipe } from "@oofp/core/pipe";
import * as E from "@oofp/core/either";

interface Order {
  items: Array<{ price: number; qty: number }>;
  discount: number;
}

const validateOrder = (data: unknown): E.Either<string, Order> =>
  typeof data === "object" && data !== null
    ? E.right(data as Order)
    : E.left("Invalid order data");

const calculateTotal = (order: Order): Order & { total: number } => ({
  ...order,
  total: order.items.reduce((sum, item) => sum + item.price * item.qty, 0),
});

const applyDiscount = (rate: number) => (order: Order & { total: number }) => ({
  ...order,
  total: order.total * (1 - rate),
});

const processOrder = (rawData: unknown) =>
  pipe(
    rawData,
    validateOrder,
    E.map(calculateTotal),
    E.map(applyDiscount(0.1)),
  );

Enter fullscreen mode Exit fullscreen mode

每个步骤做一件事。管道就是组合。添加税费计算只需一行:E.map(applyTax(0.08))

使用 TaskEither 的异步管道

import { pipe } from "@oofp/core/pipe";
import * as TE from "@oofp/core/task-either";

interface User {
  id: string;
  name: string;
}

interface Order {
  id: string;
  userId: string;
  total: number;
}

const fetchUser = (id: string): TE.TaskEither<Error, User> =>
  TE.tryCatch(
    () => fetch(`/api/users/${id}`).then((r) => r.json()),
    (err) => new Error(`Failed to fetch user: ${err}`),
  );

const fetchOrders = (userId: string): TE.TaskEither<Error, Order[]> =>
  TE.tryCatch(
    () => fetch(`/api/orders?userId=${userId}`).then((r) => r.json()),
    (err) => new Error(`Failed to fetch orders: ${err}`),
  );

const calculateTotals = (orders: Order[]) =>
  orders.reduce((sum, o) => sum + o.total, 0);

const formatReport = (total: number) => `Total revenue: $${total.toFixed(2)}`;

const fetchAndProcess = (id: string) =>
  pipe(
    fetchUser(id),
    TE.chain((user) => fetchOrders(user.id)),
    TE.map(calculateTotals),
    TE.map(formatReport),
  );

Enter fullscreen mode Exit fullscreen mode

如果 fetchUser 失败,fetchOrders 永远不会运行。错误会通过管道传播,而无需任何 try-catch 块。

结合 pipe 和 flow

一个常见模式:用 flow 定义小型管道,用 pipe 编排它们:

import { pipe } from "@oofp/core/pipe";
import { flow } from "@oofp/core/flow";
import * as E from "@oofp/core/either";

const parseNumber = flow(
  (s: string) => Number(s),
  (n) => (isNaN(n) ? E.left("Invalid number" as const) : E.right(n)),
);

const ensureRange = (min: number, max: number) =>
  flow(
    E.iif((n: number) => n >= min && n <= max),
    E.mapLeft(() => `Must be between ${min} and ${max}` as const),
  );

const parsePercentage = (input: string) =>
  pipe(
    parseNumber(input),
    E.chain(ensureRange(0, 100)),
  );

parsePercentage("42");   // Right(42)
parsePercentage("-5");   // Left("Must be between 0 and 100")
parsePercentage("abc");  // Left("Invalid number")

Enter fullscreen mode Exit fullscreen mode

flow 创建构建块。pipe 组装它们。

pipe 与方法链

你可能会想:"这看起来像 .then().then()array.map().filter()。" 方法链和 pipe 解决类似问题,但 pipe 更通用。

方法链只适用于特定类上定义的方法:

// Method chaining — only works with Array's built-in methods
const result = [1, 2, 3, 4, 5]
  .filter((n) => n > 2)
  .map((n) => n * 2)
  .reduce((sum, n) => sum + n, 0);

Enter fullscreen mode Exit fullscreen mode

如果你需要在链中间调用另一个模块中的工具函数呢?你做不到。你必须中断链,分配给一个变量,然后开始一个新链。

pipe 可以与任何函数一起工作,来自任何模块:

import { pipe } from "@oofp/core/pipe";
import * as L from "@oofp/core/list";

const result = pipe(
  [1, 2, 3, 4, 5],
  L.filter((n) => n > 2),
  L.map((n) => n * 2),
  L.reduce(0, (sum, n) => sum + n),
  formatAsUSD,        // from your utils module
  addTaxDisclaimer,   // from another module
);

Enter fullscreen mode Exit fullscreen mode

你可以混合来自任何来源的函数。没有类层次结构。没有原型链。只有函数输入,值输出。

这也使测试更简单。管道中的每个函数都是一个独立的单元,可以独立测试。无需实例化类或模拟方法。

TC39 管道操作符

JavaScript 有一个 stage 2 提案,用于管道操作符 |>

// Proposed syntax (not yet available)
const result = input
  |> validate(%)
  |> removeSpaces(%)
  |> trim(%)
  |> uppercase(%);

Enter fullscreen mode Exit fullscreen mode

它解决了 pipe 解决的相同问题。但截至 2025 年,该提案已在 stage 2 停留多年。它需要使用实验性 Babel 插件进行转译。它经历了多次语法修订。它提供的类型安全并不比 TypeScript 已经提供的更多。

来自 @oofp/core/pipepipe 今天就为你提供了相同的易用性:

  • 在任何 TypeScript 项目中零配置工作。
  • 通过整个链的完整类型推断。
  • 无需实验性标志或转译器插件。
  • 现在可用,而不是等待委员会批准。

如果管道操作符最终进入语言,从 pipe 迁移将是直截了当的。在此之前,pipe 是实际的选择。

何时使用每个

工具 输入 返回 使用时机
pipe(value, f, g, h) 一个值 + 函数 最终结果 你有一个值并想立即转换它
flow(f, g, h) 仅函数 一个新函数 你想要一个可重用的管道稍后应用
compose(h, g, f) 仅函数(反向) 一个新函数 你想要从右到左(数学)顺序

决策树很简单:

  • 已经有值了? 使用 pipe
  • 构建可重用的转换? 使用 flow
  • 更喜欢数学符号? 使用 compose

在实践中,你会 80% 的时间使用 pipe,19% 的时间使用 flow,很少使用 compose。没关系。它们都有完整的 TypeScript 推断,它们都干净地组合,它们都来自 @oofp/core

开始使用

安装核心包:

npm install @oofp/core

Enter fullscreen mode Exit fullscreen mode

导入你需要的:

import { pipe } from "@oofp/core/pipe";
import { flow } from "@oofp/core/flow";
import { compose } from "@oofp/core/compose";

Enter fullscreen mode Exit fullscreen mode

要深入了解每个工具的更多示例和 API 细节,请参阅 Pipe、Flow 和 Compose 文档

pipe 开始。替换你代码库中的一个嵌套函数调用。一旦阅读顺序与执行顺序匹配,你就不会想回去了。