Skip to content

教程:使用 Remeda(类型友好的工具库)

当你需要对数组/对象做映射、过滤、分组、去重、排序等常见操作时,lodash 很好用;但在 TypeScript 项目里,你可能希望“类型推导更强、管道化(pipeline)更顺手”。

Remeda 就是为 TypeScript 而生的实用函数库:

  • 轻量、ESM 友好,按需导入,适合 ArenaPro 环境;
  • pipe() 风格让数据流更清晰;
  • 强类型推导,链式处理更安全;
  • 大量 API 与 lodash 思路接近,迁移成本低。

一个小故事:你要做一个排行榜,把“原始分数”先去掉异常值,再做归一化,然后按分数倒序取前 5 名。结果你写了一堆中间变量:valid = ...normalized = ...sorted = ...top5 = ...,来回在文件里滚动找变量定义,还担心类型没跟上。后来你换成 Remeda 的 pipe()

ts
import { pipe, filter, map, sortBy, take } from "remeda";

const top5 = pipe(
  players,
  filter((p) => p.score > 0),
  map((p) => ({ ...p, score: Math.min(100, p.score) })),
  sortBy([(p) => -p.score]),
  take(5)
);

数据从左到右一眼到底,类型一路跟随,再也不用在中间变量里迷路。

本文从浅到深介绍 Remeda 的安装、核心用法、常见场景与迁移建议。

1. 安装

bash
npm install remeda

Remeda 为 ESM 友好库,ArenaPro 环境可直接按需导入使用。

2. 最小上手:pipe() 与基础操作

ts
import { pipe, map, filter } from "remeda";

const input = [
  { id: "p1", score: 70 },
  { id: "p2", score: 50 },
  { id: "p3", score: 90 },
];

const result = pipe(
  input,
  filter((x) => x.score >= 60),
  map((x) => ({ ...x, rank: "pass" as const }))
);

// result: Array<{ id: string; score: number; rank: "pass" }>
  • pipe(value, ...ops):从左到右依次处理数据,返回最终结果;
  • 类型推导贯穿全流程,不需要手动断言。

3. 常见实战 10 招

下面选取高频场景,示例均具有良好的类型推导。

3.1 分组:groupBy

ts
import { groupBy } from "remeda";

const players = [
  { id: "a", team: "A" },
  { id: "b", team: "B" },
  { id: "c", team: "A" },
];

const grouped = groupBy(players, (p) => p.team);
// { A: [{...}, {...}], B: [{...}] }

3.2 去重:uniqBy

ts
import { uniqBy } from "remeda";

const uniqPlayers = uniqBy([{ id: 1 }, { id: 1 }, { id: 2 }], (p) => p.id);

3.3 排序与选取:sortBy, minBy, maxBy

ts
import { sortBy, maxBy } from "remeda";

const ranked = sortBy(
  [
    { id: "p1", score: 70 },
    { id: "p2", score: 90 },
  ],
  [(x) => -x.score]
);

const top = maxBy(ranked, (x) => x.score); // 推导为 { id: string; score: number } | undefined

3.4 挑选与剔除:pick, omit

ts
import { pick, omit } from "remeda";

const brief = pick({ id: "p1", hp: 80, pos: [0, 0, 0] }, ["id", "hp"]);
const withoutPos = omit({ id: "p1", hp: 80, pos: [0, 0, 0] }, ["pos"]);

3.5 累计与折叠:reduce

ts
import { reduce } from "remeda";

const totalScore = reduce(
  [{ score: 10 }, { score: 20 }],
  (acc, x) => acc + x.score,
  0
);

3.6 管道组合:pipe + 多步处理

ts
import { pipe, filter, map, groupBy } from "remeda";

const groups = pipe(
  [
    { id: "p1", score: 70 },
    { id: "p2", score: 50 },
    { id: "p3", score: 90 },
  ],
  filter((x) => x.score >= 60),
  map((x) => ({ ...x, rank: "pass" as const })),
  groupBy((x) => x.rank)
);

3.7 安全访问:getPath, setPath

ts
import { getPath, setPath } from "remeda";

const obj = { cfg: { video: { volume: 0.8 } } };
const vol = getPath(obj, ["cfg", "video", "volume"]); // number | undefined

const updated = setPath(obj, ["cfg", "video", "volume"], 1.0);
// 返回新对象,不会修改原对象(不可变友好)

3.8 条件工具:when, tap

ts
import { pipe, when, tap } from "remeda";

const output = pipe(
  { hp: 90 },
  when(
    (x) => x.hp < 100,
    (x) => ({ ...x, hp: x.hp + 10 })
  ),
  tap((x) => console.log("after:", x))
);

3.9 谓词与类型守卫(Type Guard)

ts
import { isTruthy, filter } from "remeda";

const mixed = ["a", "", null, "b"] as Array<string | null>;
const cleaned = filter(mixed, isTruthy);
// cleaned: string[]

3.10 与数组方法协作:toPairs, fromPairs

ts
import { toPairs, fromPairs } from "remeda";

const obj = { A: 1, B: 2 };
const pairs = toPairs(obj); // Array<["A"|"B", number]>
const back = fromPairs(pairs);

4. 与 lodash-es 的迁移建议

  • 命名与语义:很多 API 与 lodash 接近,但参数顺序/返回不可变等细节可能不同,阅读官方文档很重要;
  • 建议逐模块替换:在新代码中直接使用 Remeda;旧代码保持 lodash-es,分阶段迁移;
  • 使用 pipe() 代替链式调用,避免中间变量,提升可读性与类型连贯性。

迁移对比(示例):

ts
// lodash-es
import { chain } from "lodash-es";
const result1 = chain(arr).filter(p).map(m).groupBy(g).value();

// remeda
import { pipe, filter, map, groupBy } from "remeda";
const result2 = pipe(arr, filter(p), map(m), groupBy(g));

5. ArenaPro 场景示例

  • 排行榜构建:过滤无效分数 → 评分归一化 → 排序 → 取 Top N;
  • 任务系统:按类型分组 → 统计进度 → 选择下一步目标;
  • 资源清单:汇总/去重/合并配置 → 生成可视化表格或导出。
ts
import { pipe, filter, map, sortBy, take } from "remeda";

type Player = { id: string; score: number };

const top5 = pipe(
  [
    { id: "p1", score: 70 },
    { id: "p2", score: 0 },
    { id: "p3", score: 90 },
  ] as Player[],
  filter((p) => p.score > 0),
  map((p) => ({ ...p, score: Math.min(100, p.score) })),
  sortBy([(p) => -p.score]),
  take(5)
);

6. 性能与注意事项

  • Remeda 基于不可变思路,链式处理会创建新对象/数组;在高频路径(如每帧 tick)应评估成本;
  • 充分利用按需导入与 ESM Tree-shaking,避免无意引入整库;
  • 与 Zod 配合:先在 IO 边界用 Zod 校验/转换,再用 Remeda 做业务层整理,职责清晰。

7. 常见问题(FAQ)

  • 需要 polyfill 吗?
    • Remeda 基于现代 JS/TS,通常不需要额外 polyfill。确保你的构建目标与运行环境兼容。
  • 和 lodash-es 二选一?
    • 不必强制统一。新代码优先用 Remeda,旧代码逐步迁移即可。
  • Array.prototype 的 map/filter 有何优势?
    • pipe() 组合可读性更高,类型推导在多步链路中更稳健;大量对象操作/分组/选择等 API 更丰富。

8. 参考资料

神岛实验室