npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

linqable

v0.3.0

Published

TypeScript-first LINQ-style chainable collection utilities for arrays and iterables.

Readme

linqable

TypeScript first 的前端集合操作库,参考 C# LINQ,为数组、IterableSetMapNodeList 等数据提供优雅的链式查询能力。

import { from } from "linqable";

const names = from(users)
  .where((x) => x.age > 18)
  .orderBy((x) => x.name)
  .select((x) => x.name)
  .toArray();

特性

  • 零第三方运行时依赖。
  • TypeScript first,导出完整类型声明。
  • 支持 TypeScript type predicate,where()whereIf(true, ...)first()takeWhile() 等方法可以自动缩小联合类型。
  • 支持数组、IterableSetMap、类数组对象。
  • wherewhereIfselectskiptakeconcat 等操作优先懒执行。
  • 连续的 where() / select() 会在常见场景下自动融合,减少中间迭代层。
  • 数组和类数组上的 count()last()elementAt()reverse() 会优先走下标快路径。
  • orderBy().thenBy() 支持稳定的多级排序,并针对多条件排序减少中间对象分配。
  • 补齐 range()repeat()leftJoin()pairwise()squeeze() 等常用序列处理能力。
  • 适合前端处理表格、列表、筛选、分组、报表和聚合数据。
  • 所有公开 API 都带中文 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;
  salary: number;
};

const users: User[] = [
  { id: 1, name: "Alice", age: 28, department: "R&D", salary: 30000 },
  { id: 2, name: "Bob", age: 17, department: "Sales", salary: 12000 },
  { id: 3, name: "Cathy", age: 33, department: "R&D", salary: 36000 },
];

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

数据源

from(source) 是默认入口。

from([1, 2, 3]);
from(new Set([1, 2, 3]));
from(new Map([["a", 1], ["b", 2]])); // 元素类型为 [string, number]
from(document.querySelectorAll("li"));

也可以直接创建数字范围或重复值序列:

import { range, repeat } from "linqable";

range(3, 4).toArray();     // [3, 4, 5, 6]
repeat("x", 3).toArray();  // ["x", "x", "x"]

还可以创建空序列:

import { empty } from "linqable";

const values = empty<number>().defaultIfEmpty(0).toArray();

懒执行与物化

多数转换操作不会立即遍历数据源,只有在 toArray()sum()first()forEach() 等终结操作,或使用 for...of 迭代时才开始读取。

懒执行方法包括:

where
whereIf
select
selectMany
distinct
distinctBy
skip
take
skipWhile
takeWhile
concat
append
prepend
union
intersect
except
chunk
zip
defaultIfEmpty

TypeScript 类型守卫

如果你的数据是联合类型,可以把类型守卫直接传给 where(),后面的链条会自动拿到缩小后的类型。

type UserRow = { kind: "user"; name: string };
type OrderRow = { kind: "order"; amount: number };
type Row = UserRow | OrderRow;

declare const rows: Row[];

const isUser = (row: Row): row is UserRow => row.kind === "user";

const names = from(rows)
  .where(isUser)
  .select((user) => user.name)
  .toArray();

如果使用 whereIf(),只有当 condition 是字面量 true 时,TypeScript 才会把类型缩小到守卫后的类型。普通 boolean 在编译期无法确认筛选一定发生,因此会保留原始元素类型。

同样的类型缩小也适用于会返回元素的条件方法,例如 first()firstOrDefault()single()singleOrDefault()last()lastOrDefault()takeWhile()

const firstUser = from(rows).first(isUser); // UserRow
const maybeUser = from(rows).firstOrDefault(undefined, isUser); // UserRow | undefined

all() 也支持类型守卫。判断通过后,当前查询对象会在 TypeScript 中缩小为更具体的 Enumerable 类型:

const query = from(rows);

if (query.all(isUser)) {
  query.select((user) => user.name);
}

where/select 融合

where()select() 仍然是懒执行的,但 linqable 会把常见的连续链条自动合并成更少的内部迭代层。

const result = from(rows)
  .where((row) => row.enabled)
  .where((row) => row.score > 80)
  .select((row) => row.name)
  .select((name) => name.trim())
  .toArray();

上面这种不使用索引参数的链条,会尽量在一层循环里完成过滤和映射,减少 generator 层层传递的开销。

如果回调声明了索引参数,linqable 会保守地回到普通懒迭代逻辑,确保每一步的 index 语义不变:

from(rows)
  .where((row, index) => index > 0)
  .select((row, index) => ({ row, index }));

数组和类数组快路径

如果数据源本身支持 length 和下标访问,例如数组、NodeListHTMLCollection 或普通类数组对象,linqable 会记住这一点。

所以这些直达操作不会从头遍历:

const rows = from(largeArray);

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

如果中间加了 where()select()skip() 等转换,结果已经不再等同于原始下标序列,linqable 会回到正常的懒迭代逻辑,保证语义正确。

需要全量数据的方法会在迭代或调用时物化。数组和类数组上的 count()reverse() 等直达场景会优先使用上面的下标快路径:

orderBy
orderByDescending
thenBy
thenByDescending
groupBy
reverse
toArray
toMap
toSet
sum
average
count
min
max
minBy
maxBy
aggregate

常用 API

过滤与映射

const result = from(products)
  .where((x) => x.enabled)
  .select((x) => ({ label: x.name, value: x.id }))
  .toArray();

需要根据外部条件动态追加筛选时,可以使用 whereIf()。当条件为 false 时,它会直接保留当前查询,不会调用谓词:

const result = from(products)
  .where((x) => x.enabled)
  .whereIf(keyword !== "", (x) => x.name.includes(keyword))
  .whereIf(category !== undefined, (x) => x.category === category)
  .select((x) => ({ label: x.name, value: x.id }))
  .toArray();

排序

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

排序会在迭代或 toArray() 时读取完整序列,并保持稳定排序:多个元素的排序键都相等时,会保留它们在原始序列中的先后顺序。内部会缓存每个排序条件的 key,并排序下标数组,减少为每个元素创建临时对象的开销。

可以传入自定义比较器:

from(files).orderBy(
  (x) => x.name,
  (a, b) => a.localeCompare(b, "zh-CN"),
);

分组

groupBy 返回 Grouping<TKey, TElement>,包含 keyvalues,同时自身也可以迭代。

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

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

去重

from([1, 2, 2, 3]).distinct().toArray(); // [1, 2, 3]

from(users)
  .distinctBy((user) => user.id)
  .toArray();

如果只想压缩相邻重复值,用 squeeze()。它不会做全局去重,后面再次出现的相同值仍会保留:

from([1, 1, 2, 2, 1]).squeeze().toArray(); // [1, 2, 1]

from(statusLogs)
  .squeezeBy((row) => row.status)
  .toArray();

分页

const page = from(rows)
  .skip((pageIndex - 1) * pageSize)
  .take(pageSize)
  .toArray();

聚合

const total = from(orders).sum((x) => x.amount);
const average = from(orders).average((x) => x.amount);
const count = from(orders).count((x) => x.status === "paid");
const best = from(users).maxBy((x) => x.score);

连接

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

左连接会保留没有匹配项的左侧元素,此时内部元素参数为 undefined

const rows = from(users)
  .leftJoin(
    orders,
    (user) => user.id,
    (order) => order.userId,
    (user, order) => ({
      name: user.name,
      amount: order?.amount ?? 0,
    }),
  )
  .toArray();

相邻配对与序列比较

from([1, 2, 3]).pairwise().toArray(); // [[1, 2], [2, 3]]
from([1, 2, 3]).sequenceEqual([1, 2, 3]); // true
from(["a", "b", "a"]).indexOf("b"); // 1

转换为原生集合

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

键值对序列可直接转换为 Map

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

完整方法列表

入口:

  • from
  • empty
  • range
  • repeat

基础方法:

  • where
  • whereIf
  • select
  • groupBy
  • orderBy
  • thenBy
  • orderByDescending
  • thenByDescending
  • distinct
  • distinctBy
  • squeeze
  • squeezeBy
  • first
  • firstOrDefault
  • single
  • singleOrDefault
  • sum
  • average
  • count
  • any
  • all
  • contains
  • indexOf
  • sequenceEqual
  • skip
  • take
  • skipWhile
  • takeWhile
  • concat
  • append
  • prepend
  • reverse
  • toArray
  • toMap
  • toSet
  • forEach

高级方法:

  • selectMany
  • join
  • leftJoin
  • groupJoin
  • union
  • intersect
  • except
  • chunk
  • zip
  • pairwise
  • min
  • max
  • minBy
  • maxBy
  • aggregate
  • defaultIfEmpty
  • elementAt
  • elementAtOrDefault
  • last
  • lastOrDefault

错误行为

这些方法在找不到元素或输入不合法时会抛出错误:

  • first():没有元素或没有匹配元素时抛错。
  • single():没有匹配元素或匹配多个元素时抛错。
  • average():空序列抛错。
  • min() / max() / minBy() / maxBy():空序列抛错。
  • elementAt(index):索引越界抛错。
  • range(start, count) / repeat(value, count)count 必须是非负整数。
  • skip(count) / take(count)count 必须是非负整数。
  • chunk(size)size 必须是正整数。

如果希望失败时返回默认值,可以使用:

firstOrDefault(defaultValue)
singleOrDefault(defaultValue)
lastOrDefault(defaultValue)
elementAtOrDefault(index, defaultValue)
defaultIfEmpty(defaultValue)

类型提示

linqable 会尽量保留链式调用中的类型信息:

const ids: number[] = from(users)
  .where((user) => user.age >= 18)
  .select((user) => user.id)
  .toArray();

const map: Map<number, string> = from(users).toMap(
  (user) => user.id,
  (user) => user.name,
);

构建与验证

项目使用 TypeScript 编译为 ESM:

pnpm run typecheck
pnpm run test
pnpm run build

test 使用 Node.js 内置测试运行器,会先构建 dist,再测试实际发布产物。

发布

发布前建议运行:

pnpm run prepublishOnly

包入口:

  • ESM: dist/index.js
  • Types: dist/index.d.ts

License

MIT