C#程序员的前端LINQ解决方案

0 阅读15分钟

linqable:一个为前端而生的 TypeScript LINQ 风格集合操作库

在前端项目里,我们几乎每天都在处理集合数据:表格列表、接口返回的数组、筛选条件、分页、分组统计、报表汇总、下拉选项、权限菜单、订单明细、库存数据。

原生 JavaScript 已经提供了 mapfilterreducesort 等方法,但当业务逻辑稍微复杂一点时,代码很容易变成这样:

const activeUsers = users
  .filter((user) => user.enabled)
  .filter((user) => user.age >= 18)
  .sort((a, b) => {
    const deptCompare = a.department.localeCompare(b.department);
    if (deptCompare !== 0) return deptCompare;
    return b.score - a.score;
  })
  .map((user) => ({
    id: user.id,
    label: `${user.department} - ${user.name}`,
  }));

这还只是过滤、排序和映射。如果再加上分组、聚合、连接、去重、分页、集合差集,代码会越来越难读。

linqable 想解决的就是这个问题:用接近 C# LINQ 的链式写法,让前端集合处理更清晰、更稳定,也更适合 TypeScript 项目。

import { from } from "linqable";

const activeUsers = from(users)
  .where((user) => user.enabled)
  .where((user) => user.age >= 18)
  .orderBy((user) => user.department)
  .thenByDescending((user) => user.score)
  .select((user) => ({
    id: user.id,
    label: `${user.department} - ${user.name}`,
  }))
  .toArray();

linqable 是什么

linqable 是一个 TypeScript first 的前端集合操作库。

它参考 C# LINQ 的 API 风格,为数组、IterableSetMapNodeList 等数据源提供统一的链式查询能力。

它适合这些场景:

  • 后台管理系统中的表格筛选、排序、分页。
  • 报表页面中的分组、聚合、统计。
  • 接口数据清洗、格式转换、字段映射。
  • 多个数据源之间的关联查询。
  • 列表去重、交集、并集、差集处理。
  • 前端页面里的轻量数据分析。

它的设计目标很明确:

  • 零第三方运行时依赖。
  • TypeScript 类型友好。
  • API 简洁稳定。
  • 尽量懒执行,减少不必要的中间数组。
  • 自动融合常见的连续 where() / select() 链条,减少中间迭代层。
  • 对数组、NodeList 等类数组数据保留下标访问能力,常见直达操作不用从头遍历。
  • 对前端开发者常见业务足够顺手。
  • 所有公开方法都有中文 JSDoc,编辑器悬浮即可看到说明。

安装

npm install linqable

也可以使用 pnpm 或 yarn:

pnpm add linqable
yarn add linqable

快速开始

import { from } from "linqable";

type User = {
  id: number;
  name: string;
  age: number;
  department: string;
  score: number;
};

const users: User[] = [
  { id: 1, name: "Alice", age: 28, department: "R&D", score: 95 },
  { id: 2, name: "Bob", age: 17, department: "Sales", score: 81 },
  { id: 3, name: "Cathy", age: 33, department: "R&D", score: 88 },
  { id: 4, name: "David", age: 24, department: "Sales", score: 92 },
];

const names = from(users)
  .where((user) => user.age >= 18)
  .orderBy((user) => user.department)
  .thenByDescending((user) => user.score)
  .select((user) => user.name)
  .toArray();

console.log(names);
// ["Alice", "Cathy", "David"]

你可以把 from(users) 理解为把普通数组包装成一个可链式查询的集合。中间操作会继续返回新的查询对象,最后通过 toArray()sum()first()count() 等方法得到结果。

支持哪些数据源

linqable 不只支持数组。只要是 Iterable 或类数组对象,都可以使用 from() 包装。

import { from } from "linqable";

from([1, 2, 3]);
from(new Set([1, 2, 3]));
from(new Map([["a", 1], ["b", 2]]));
from(document.querySelectorAll("li"));

Map 会被视为键值对序列:

const pairs = from(new Map([["a", 1], ["b", 2]])).toArray();

// [["a", 1], ["b", 2]]

为什么不是继续写 filter/map/reduce

原生数组方法很好,但它们有几个常见问题:

第一,复杂业务会产生大量中间数组。

const result = users
  .filter((x) => x.enabled)
  .map((x) => x.orders)
  .flat()
  .filter((x) => x.status === "paid")
  .map((x) => x.amount);

第二,多级排序写起来很繁琐。

users.sort((a, b) => {
  const dept = a.department.localeCompare(b.department);
  if (dept !== 0) return dept;
  const score = b.score - a.score;
  if (score !== 0) return score;
  return a.name.localeCompare(b.name);
});

第三,分组、连接、集合运算不是原生数组 API 的强项。

比如你想做:

  • 按部门分组。
  • 每组统计人数、总薪资、平均薪资。
  • 按总薪资倒序排序。
  • 只保留前 5 个部门。

linqable 会自然很多:

const departmentReport = from(users)
  .groupBy((user) => user.department)
  .select((group) => ({
    department: group.key,
    count: group.values.length,
    totalSalary: from(group).sum((user) => user.salary),
    averageSalary: from(group).average((user) => user.salary),
  }))
  .orderByDescending((row) => row.totalSalary)
  .take(5)
  .toArray();

核心概念:转换、排序、分组、终结

linqable 的方法大致可以分成四类。

1. 转换方法

转换方法会返回新的 Enumerable,可以继续链式调用。

from(users)
  .where((user) => user.enabled)
  .select((user) => user.name)
  .skip(10)
  .take(20)
  .toArray();

常见转换方法:

  • where
  • select
  • selectMany
  • skip
  • take
  • skipWhile
  • takeWhile
  • concat
  • append
  • prepend
  • reverse
  • chunk
  • zip
  • defaultIfEmpty

2. 排序方法

支持多级排序:

const rows = from(users)
  .orderBy((user) => user.department)
  .thenByDescending((user) => user.score)
  .thenBy((user) => user.name)
  .toArray();

也支持自定义比较器:

const files = from(fileList)
  .orderBy(
    (file) => file.name,
    (a, b) => a.localeCompare(b, "zh-CN"),
  )
  .toArray();

3. 分组和连接

分组:

const groups = from(orders)
  .groupBy((order) => order.status)
  .toArray();

for (const group of groups) {
  console.log(group.key, group.values);
}

连接:

const rows = from(users)
  .join(
    departments,
    (user) => user.departmentId,
    (department) => department.id,
    (user, department) => ({
      userName: user.name,
      departmentName: department.name,
    }),
  )
  .toArray();

分组连接:

const rows = from(users)
  .groupJoin(
    orders,
    (user) => user.id,
    (order) => order.userId,
    (user, matchedOrders) => ({
      name: user.name,
      orderCount: matchedOrders.count(),
      totalAmount: matchedOrders.sum((order) => order.amount),
    }),
  )
  .toArray();

4. 终结方法

终结方法会真正产生结果。

from(users).toArray();
from(users).count();
from(users).first();
from(users).sum((user) => user.salary);
from(users).average((user) => user.score);
from(users).toMap((user) => user.id);
from(users).toSet();

常见终结方法:

  • toArray
  • toMap
  • toSet
  • forEach
  • first
  • firstOrDefault
  • single
  • singleOrDefault
  • last
  • lastOrDefault
  • elementAt
  • elementAtOrDefault
  • sum
  • average
  • count
  • any
  • all
  • contains
  • min
  • max
  • minBy
  • maxBy
  • aggregate

懒执行:更少的中间数组

linqable 会尽量懒执行。像 whereselectskiptake 这些方法不会立刻遍历数据源。

const query = from(users)
  .where((user) => user.enabled)
  .select((user) => user.name);

// 此时还没有真正遍历 users

const result = query.toArray();
// 到这里才开始执行

这意味着你可以先组合查询逻辑,最后再决定如何消费结果。

take 还会在拿到足够结果后停止读取数据源:

function* numbers() {
  for (let i = 1; i <= 100000; i += 1) {
    yield i;
  }
}

const firstThreeEvenNumbers = from(numbers())
  .where((x) => x % 2 === 0)
  .take(3)
  .toArray();

// [2, 4, 6]

where/select 融合:链式写法,不必为每一层付全部成本

链式 API 很好读,但朴素实现里,每个 where()select() 都可能多包一层迭代器。链条越长,数据在内部管道里传递的层数就越多。

linqable 会针对最常见的过滤和映射链做融合:

const result = from(users)
  .where((user) => user.enabled)
  .where((user) => user.age >= 18)
  .select((user) => user.name)
  .select((name) => name.trim())
  .toArray();

大白话说,能合在一趟里做的过滤和映射,就尽量不要拆成好几趟内部管道。这样代码还是清楚的链式写法,但运行时少走一些中间层。

当前会优先优化这些常见组合:

  • where().where()
  • where().select()
  • select().select()
  • where().where().select().select() 这类组合链

如果回调需要索引,linqable 会回到普通懒迭代方式:

const rows = from(users)
  .where((user, index) => index > 0)
  .select((user, index) => ({ user, index }))
  .toArray();

这是为了保证 index 语义准确。比如 where() 的 index 和 select() 的 index 可能来自不同阶段,不能为了性能把它们混在一起。

数组和类数组:能直接拿,就不绕远路

数组、NodeListHTMLCollection 这类数据都有一个共同点:它们知道自己的 length,也能通过下标直接取值。

linqable 会保留这个信息。就是如果数据已经像书架一样编好了号,取第 50000 本书时就直接去第 50000 个位置,不会从第一本开始一本本数。

const rows = from(largeArray);

rows.count();          // 直接读取 length
rows.last();           // 直接读取最后一个下标
rows.elementAt(50000); // 直接读取指定下标
rows.reverse();        // 倒序按下标读取

这类优化只发生在语义明确的直达场景里。如果你写了 where()select() 等转换,结果已经变成新的逻辑序列,linqable 会继续使用正常的懒迭代方式,保证结果不会因为快路径而变味。

场景一:表格筛选、排序、分页

前端后台系统最常见的场景之一就是表格处理。

import { from } from "linqable";

type Product = {
  id: number;
  name: string;
  category: string;
  price: number;
  stock: number;
  enabled: boolean;
};

function queryProducts(
  products: Product[],
  keyword: string,
  category: string | undefined,
  pageIndex: number,
  pageSize: number,
) {
  const query = from(products)
    .where((product) => product.enabled)
    .where((product) => product.name.includes(keyword))
    .where((product) => category === undefined || product.category === category)
    .orderBy((product) => product.category)
    .thenByDescending((product) => product.stock)
    .thenBy((product) => product.price);

  return {
    total: query.count(),
    rows: query
      .skip((pageIndex - 1) * pageSize)
      .take(pageSize)
      .toArray(),
  };
}

这段代码很接近业务描述:

  1. 只看启用商品。
  2. 按关键词过滤。
  3. 按分类过滤。
  4. 按分类、库存、价格排序。
  5. 统计总数。
  6. 返回当前页。

场景二:销售报表分组统计

type Order = {
  id: number;
  region: string;
  status: "paid" | "pending" | "refunded";
  amount: number;
};

const orders: Order[] = [
  { id: 1, region: "East", status: "paid", amount: 120 },
  { id: 2, region: "East", status: "paid", amount: 80 },
  { id: 3, region: "West", status: "pending", amount: 50 },
  { id: 4, region: "West", status: "paid", amount: 200 },
  { id: 5, region: "North", status: "refunded", amount: 20 },
];

const report = from(orders)
  .where((order) => order.status === "paid")
  .groupBy((order) => order.region)
  .select((group) => {
    const rows = from(group);

    return {
      region: group.key,
      orderCount: rows.count(),
      totalAmount: rows.sum((order) => order.amount),
      averageAmount: rows.average((order) => order.amount),
      maxOrder: rows.maxBy((order) => order.amount),
    };
  })
  .orderByDescending((row) => row.totalAmount)
  .toArray();

结果示例:

[
  {
    region: "West",
    orderCount: 1,
    totalAmount: 200,
    averageAmount: 200,
    maxOrder: { id: 4, region: "West", status: "paid", amount: 200 },
  },
  {
    region: "East",
    orderCount: 2,
    totalAmount: 200,
    averageAmount: 100,
    maxOrder: { id: 1, region: "East", status: "paid", amount: 120 },
  },
]

场景三:关联用户、部门和订单

当页面需要把多个接口数据合并展示时,joingroupJoin 会很有用。

type User = {
  id: number;
  name: string;
  departmentId: number;
};

type Department = {
  id: number;
  name: string;
};

type Order = {
  id: number;
  userId: number;
  amount: number;
};

const userRows = from(users)
  .join(
    departments,
    (user) => user.departmentId,
    (department) => department.id,
    (user, department) => ({
      user,
      department,
    }),
  )
  .groupJoin(
    orders,
    (row) => row.user.id,
    (order) => order.userId,
    (row, userOrders) => ({
      userId: row.user.id,
      userName: row.user.name,
      departmentName: row.department.name,
      orderCount: userOrders.count(),
      totalAmount: userOrders.sum((order) => order.amount),
    }),
  )
  .orderBy((row) => row.departmentName)
  .thenByDescending((row) => row.totalAmount)
  .toArray();

这类代码在前端 BFF、管理后台、数据看板里非常常见。使用链式 API 后,数据流会比散落的临时变量更清晰。

场景四:库存对账和集合运算

unionintersectexcept 适合处理集合关系。

const warehouseA = ["A-1", "B-1", "C-1"];
const warehouseB = ["B-1", "D-1", "E-1"];
const discontinued = ["C-1", "E-1"];
const promoted = ["A-1", "B-1", "X-1"];

const activeSkus = from(warehouseA)
  .union(warehouseB)
  .except(discontinued)
  .orderBy((sku) => sku)
  .toArray();

const promotedAndActive = from(activeSkus)
  .intersect(promoted)
  .toArray();

console.log(activeSkus);
// ["A-1", "B-1", "D-1"]

console.log(promotedAndActive);
// ["A-1", "B-1"]

如果要分批处理,还可以接上 chunkzip

const batches = from(activeSkus)
  .chunk(2)
  .zip(["morning", "afternoon"], (skus, slot) => ({
    slot,
    skus,
  }))
  .toArray();

// [
//   { slot: "morning", skus: ["A-1", "B-1"] },
//   { slot: "afternoon", skus: ["D-1"] },
// ]

场景五:展开嵌套数据

selectMany 可以把嵌套集合展平成一层。

const departments = [
  { name: "R&D", members: ["Alice", "Cathy"] },
  { name: "Sales", members: ["Bob"] },
];

const memberNames = from(departments)
  .selectMany((department) => department.members)
  .toArray();

// ["Alice", "Cathy", "Bob"]

也可以保留父级信息:

const rows = from(departments)
  .selectMany(
    (department) => department.members,
    (department, member) => ({
      department: department.name,
      member,
    }),
  )
  .toArray();

// [
//   { department: "R&D", member: "Alice" },
//   { department: "R&D", member: "Cathy" },
//   { department: "Sales", member: "Bob" },
// ]

场景六:一个更完整的数据看板示例

下面这个例子把过滤、连接、分组、去重、展开、聚合、多级排序和分页串在一起,模拟一个区域销售看板。

import { from } from "linqable";

const customers = [
  { id: 1, name: "Alice", region: "East", active: true },
  { id: 2, name: "Bob", region: "West", active: true },
  { id: 3, name: "Cathy", region: "East", active: false },
  { id: 4, name: "David", region: "North", active: true },
];

const orders = [
  { id: 101, customerId: 1, status: "paid", amount: 120, items: ["book", "pen"] },
  { id: 102, customerId: 1, status: "paid", amount: 80, items: ["notebook"] },
  { id: 103, customerId: 2, status: "pending", amount: 50, items: ["pen"] },
  { id: 104, customerId: 2, status: "paid", amount: 200, items: ["bag", "pen"] },
  { id: 105, customerId: 3, status: "paid", amount: 300, items: ["laptop"] },
  { id: 106, customerId: 4, status: "refunded", amount: 20, items: ["clip"] },
  { id: 107, customerId: 4, status: "paid", amount: 30, items: ["paper"] },
];

const dashboard = from(customers)
  .where((customer) => customer.active)
  .join(
    from(orders).where((order) => order.status === "paid"),
    (customer) => customer.id,
    (order) => order.customerId,
    (customer, order) => ({ customer, order }),
  )
  .groupBy((row) => row.customer.region)
  .select((group) => {
    const rows = from(group);

    return {
      region: group.key,
      customers: rows
        .distinctBy((row) => row.customer.id)
        .select((row) => row.customer.name)
        .orderBy((name) => name)
        .toArray(),
      total: rows.sum((row) => row.order.amount),
      averageOrder: rows.average((row) => row.order.amount),
      orderCount: rows.count(),
      itemCount: rows.selectMany((row) => row.order.items).count(),
      topOrderId: rows.maxBy((row) => row.order.amount).order.id,
    };
  })
  .orderByDescending((row) => row.total)
  .thenBy((row) => row.region)
  .take(10)
  .toArray();

结果会是类似这样的结构:

[
  {
    region: "East",
    customers: ["Alice"],
    total: 200,
    averageOrder: 100,
    orderCount: 2,
    itemCount: 3,
    topOrderId: 101,
  },
  {
    region: "West",
    customers: ["Bob"],
    total: 200,
    averageOrder: 200,
    orderCount: 1,
    itemCount: 2,
    topOrderId: 104,
  },
  {
    region: "North",
    customers: ["David"],
    total: 30,
    averageOrder: 30,
    orderCount: 1,
    itemCount: 1,
    topOrderId: 107,
  },
]

这段代码的重点不是“少写几行”,而是让数据处理流程更像业务说明:活跃客户、已支付订单、按区域分组、统计指标、排序、取前几条。

默认值与错误行为

linqable 区分“必须存在”和“可以不存在”的场景。

如果你确定一定有元素,可以使用:

from(users).first();
from(users).single((user) => user.id === 1);
from(users).last();
from(users).elementAt(0);

如果你希望找不到时返回默认值,可以使用:

from(users).firstOrDefault(undefined);
from(users).singleOrDefault(undefined, (user) => user.id === 1);
from(users).lastOrDefault(undefined);
from(users).elementAtOrDefault(10, fallbackUser);

聚合时也有清晰的错误行为:

from([]).average(); // 空序列会抛错
from([]).min();     // 空序列会抛错
from([]).max();     // 空序列会抛错

这种设计让代码语义更明确:到底是“必须有”,还是“没有也可以”。

转换成原生集合

最后通常需要把查询结果交给 UI 组件、缓存、状态管理或其他函数。

const array = from(users).toArray();
const set = from(users).select((user) => user.department).toSet();
const map = from(users).toMap((user) => user.id);

也可以指定 Map 的值:

const nameMap = from(users).toMap(
  (user) => user.id,
  (user) => user.name,
);

如果序列本身就是键值对,可以直接转成 Map

const map = from([["a", 1] as const, ["b", 2] as const]).toMap();

API 总览

入口:

  • from
  • empty

筛选和映射:

  • where
  • select
  • selectMany

排序:

  • orderBy
  • thenBy
  • orderByDescending
  • thenByDescending
  • reverse

分组和连接:

  • groupBy
  • join
  • groupJoin

去重和集合运算:

  • distinct
  • distinctBy
  • union
  • intersect
  • except

分页和切片:

  • skip
  • take
  • skipWhile
  • takeWhile
  • chunk

聚合和判断:

  • sum
  • average
  • count
  • any
  • all
  • contains
  • min
  • max
  • minBy
  • maxBy
  • aggregate

元素获取:

  • first
  • firstOrDefault
  • single
  • singleOrDefault
  • last
  • lastOrDefault
  • elementAt
  • elementAtOrDefault
  • defaultIfEmpty

序列组合:

  • concat
  • append
  • prepend
  • zip

物化输出:

  • toArray
  • toMap
  • toSet
  • forEach

方法速查表

下面是常用方法的用途和最小示例,适合在文章中作为快速参考。

方法用途示例
from(source)创建查询对象from(users)
empty<T>()创建空序列empty<number>()
where(predicate)过滤元素from(users).where((x) => x.enabled)
select(selector)映射元素from(users).select((x) => x.name)
selectMany(selector)展开嵌套集合from(groups).selectMany((x) => x.items)
groupBy(keySelector)分组from(users).groupBy((x) => x.department)
orderBy(selector)升序排序from(users).orderBy((x) => x.name)
thenBy(selector)追加升序排序query.orderBy((x) => x.group).thenBy((x) => x.name)
orderByDescending(selector)降序排序from(users).orderByDescending((x) => x.score)
thenByDescending(selector)追加降序排序query.orderBy((x) => x.group).thenByDescending((x) => x.score)
distinct()按值去重from([1, 1, 2]).distinct()
distinctBy(selector)按键去重from(users).distinctBy((x) => x.id)
first()获取第一个元素from(users).first()
firstOrDefault(defaultValue)获取第一个元素或默认值from(users).firstOrDefault(undefined)
single(predicate)获取唯一匹配元素from(users).single((x) => x.id === id)
singleOrDefault(defaultValue, predicate)获取唯一匹配元素或默认值from(users).singleOrDefault(undefined, (x) => x.id === id)
last()获取最后一个元素from(users).last()
lastOrDefault(defaultValue)获取最后一个元素或默认值from(users).lastOrDefault(undefined)
elementAt(index)获取指定索引元素from(users).elementAt(0)
elementAtOrDefault(index, defaultValue)获取指定索引元素或默认值from(users).elementAtOrDefault(10, fallback)
sum(selector)求和from(orders).sum((x) => x.amount)
average(selector)平均值from(orders).average((x) => x.amount)
count(predicate?)计数from(users).count((x) => x.enabled)
any(predicate?)是否存在元素from(users).any((x) => x.enabled)
all(predicate)是否全部满足条件from(users).all((x) => x.enabled)
contains(value)是否包含元素from(ids).contains(1)
min()最小值from(numbers).min()
max()最大值from(numbers).max()
minBy(selector)按键取最小元素from(users).minBy((x) => x.age)
maxBy(selector)按键取最大元素from(users).maxBy((x) => x.score)
aggregate(...)自定义聚合from(numbers).aggregate(0, (sum, x) => sum + x)
skip(count)跳过前 N 个from(rows).skip(20)
take(count)获取前 N 个from(rows).take(10)
skipWhile(predicate)按条件持续跳过from(rows).skipWhile((x) => x.hidden)
takeWhile(predicate)按条件持续获取from(rows).takeWhile((x) => x.visible)
concat(...sources)连接序列from(a).concat(b, c)
append(value)末尾追加元素from(items).append(item)
prepend(value)开头添加元素from(items).prepend(item)
reverse()反转序列from(items).reverse()
join(...)内连接from(users).join(depts, userKey, deptKey, result)
groupJoin(...)分组连接from(users).groupJoin(orders, userKey, orderKey, result)
union(other)并集from(a).union(b)
intersect(other)交集from(a).intersect(b)
except(other)差集from(a).except(b)
chunk(size)分块from(rows).chunk(20)
zip(other)按位置配对from(names).zip(scores)
defaultIfEmpty(value)空序列补默认值from(rows).defaultIfEmpty(defaultRow)
toArray()输出数组from(users).toArray()
toMap(...)输出 Mapfrom(users).toMap((x) => x.id)
toSet()输出 Setfrom(tags).toSet()
forEach(action)遍历执行操作from(rows).forEach((row) => console.log(row))

和 C# LINQ 的关系

linqable 借鉴的是 C# LINQ 的思路,而不是完全复制。

它保留了前端开发者更熟悉的 JavaScript / TypeScript 使用方式:

  • 使用箭头函数作为选择器和谓词。
  • 输出原生 ArrayMapSet
  • 支持任意 Iterable
  • 类型声明面向 TypeScript 编辑器提示优化。
  • 不引入运行时依赖,不改变原始数据。

如果你写过 C# LINQ,会觉得它很自然。如果你没写过,也可以把它当作一组更完整、更统一的数组处理工具。

适合什么时候使用

适合:

  • C# 开发者写前端代码
  • 前端开发者想学C#代码
  • 前端页面中有较多列表处理逻辑。
  • 管理后台、数据看板、报表页面。
  • 需要频繁做分组、聚合、排序、分页。
  • 想把复杂的数组处理逻辑写得更可读。
  • 希望 TypeScript 类型提示更完整。

不一定适合:

  • 只做一两个简单的 mapfilter
  • 数据规模非常大且需要专门的数据处理引擎。
  • 已经使用数据库、后端查询语言或专门分析工具完成所有聚合。

项目质量

linqable 当前具备这些工程保障:

  • TypeScript 编写。
  • 生成 .d.ts 类型声明。
  • 零第三方运行时依赖。
  • Node.js 内置测试运行器测试发布产物。
  • 覆盖每个公开方法的测试示例。
  • 包含复杂链式调用测试。
  • 针对 where() / select() 融合链、索引回退、懒执行和短路行为有测试覆盖。
  • 针对数组和类数组的 count()last()elementAt()reverse() 有下标快路径测试。
  • 支持 pnpm run typecheckpnpm run testpnpm run build
  • README 可直接作为 npmjs 展示文档。

一个简单结论

如果你的前端项目里经常出现这样的代码:

data
  .filter(...)
  .map(...)
  .sort(...)
  .reduce(...)

并且你开始觉得分组、聚合、连接、去重、分页越来越难维护,那么可以试试 linqable

它不是为了替代所有数组 API,而是给复杂集合处理提供一套更完整、更清晰、更接近业务语义的表达方式。

import { from } from "linqable";

const result = from(data)
  .where(...)
  .groupBy(...)
  .select(...)
  .orderByDescending(...)
  .take(...)
  .toArray();

让前端数据处理,从“堆数组方法”,变成一条可读的查询链。

相关链接

  • git:im76ix/linqable - 欢迎 Star ⭐
  • npm 包名:linqable
  • 安装命令:npm install linqable
  • 运行时依赖:无
  • 类型支持:TypeScript first
  • 许可协议:MIT