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

tyneq

v1.0.3

Published

Lazy query pipelines for TypeScript.

Readme


import { Tyneq } from "tyneq";

const topScorers = Tyneq
  .from([
    { name: "Ada",   team: "core",  score: 84 },
    { name: "Linus", team: "infra", score: 92 },
    { name: "Grace", team: "core",  score: 97 },
  ])
  .where((p) => p.team === "core")
  .orderByDescending((p) => p.score)
  .select((p) => `${p.name} (${p.score})`)
  .toArray();
// ["Grace (97)", "Ada (84)"]

Nothing runs until .toArray(). Every operator is deferred, fully typed, and the same query can be re-evaluated as many times as you want.


What is this?

Tyneq is a LINQ-style query pipeline library for TypeScript. You compose operators on a sequence, nothing executes until you call a terminal, and you can reuse the same query without rebuilding it.

It is not a thin wrapper around Array.prototype. It is a pipeline engine with a deliberate execution model, a real query plan system, and a plugin API that lets you ship custom operators as standalone packages.


Why Tyneq?

Sequences, not cursors

Most iterator libraries give you a one-shot cursor. Once you consume it, it is gone. Tyneq sequences are re-iterable by default - call any terminal as many times as you want, each gets independent state.

const active = Tyneq.from(users)
  .where((u) => u.active)
  .orderByDescending((u) => u.score);

active.count();                         // 3
active.first().name;                    // "Grace"
active.select((u) => u.email).toArray(); // ["g@...", "l@...", "a@..."]

No re-wrapping. No rebuilding. Same query, three independent evaluations.

You always know what is happening

Every operator is explicitly streaming (O(1) memory, one element at a time) or buffering (reads the full source once, then serves from a buffer). There is no hidden materialization and no guessing about when data gets copied.

Tyneq.from(largeDataset)
  .where((x) => x.active)     // streaming
  .orderBy((x) => x.score)    // buffering - reads all matching, sorts once
  .take(10)                    // streaming - stops after 10
  .toArray();                  // terminal - kicks everything off

Query plans you can actually use

Every sequence carries a live description of its pipeline. Print it, walk it with a visitor, rewrite it with a transformer, or compile it back into an executable sequence.

import { QueryPlanPrinter, QueryPlanCompiler, QueryPlanOptimizer, tyneqQueryNode } from "tyneq";

const seq = Tyneq.from(data)
  .where((x) => x > 0)
  .where((x) => x < 100)
  .select((x) => x * 2);

// Print the plan
console.log(QueryPlanPrinter.print(seq[tyneqQueryNode]!));
// from([...])
//   -> where(<fn>)
//   -> where(<fn>)
//   -> select(<fn>)

// Compile with optimization - fuses the two where nodes
const compiler = new QueryPlanCompiler([new QueryPlanOptimizer()]);
compiler.compile(seq[tyneqQueryNode]!).toArray();

The compiler turns plans into executable sequences. Store a pipeline as metadata, optimize it, swap the data source, replay it. Pipelines become data.

Extensible to the core

Custom operators look and behave exactly like built-ins. They get registered at import time, appear on every sequence, and show up in query plans.

import { createGeneratorOperator } from "tyneq";

createGeneratorOperator({
  name: "repeatEach",
  category: "streaming",
  *generator(source: Iterable<unknown>, times: number) {
    for (const item of source) {
      for (let i = 0; i < times; i++) yield item;
    }
  },
  validate(times) {
    if (times < 1) throw new RangeError("times must be >= 1");
  },
});

declare module "tyneq" {
  interface TyneqSequence<T> {
    repeatEach(times: number): TyneqSequence<T>;
  }
}

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

Two registration styles: functional (generators, factories, terminals) and class-based (decorators with full lifecycle). Ship it as a package - consumers import once and every sequence gains the operator.


Quick comparison

| | Tyneq | Typical iterator lib | |---|---|---| | Deferred execution | yes | yes | | Re-iterable sequences | yes | often no | | Explicit streaming vs. buffering | yes | usually implicit | | Multi-key ordering (thenBy) | yes | varies | | Joins and group joins | yes | rare | | Built-in memoization | yes | rare | | Custom operator plugin API | yes | rare | | Query plan + compiler | yes | very rare | | Zero runtime dependencies | yes | varies |


Install

npm install tyneq

Requirements

  • TypeScript 5.0 or higher
  • "strictNullChecks": true in your tsconfig

No @types package needed.

Stage 3 Decorators

tyneq is implemented with TC39 Stage 3 decorators - the standardized decorator proposal that shipped in TypeScript 5.0 and requires no extra tsconfig flags. This is a different model from the legacy experimentalDecorators syntax used by NestJS, Angular, and InversifyJS.

Using built-in operators requires nothing from you. The decorator machinery lives entirely inside tyneq's compiled output. Calling .where(), .select(), or any terminal is just calling a plain method. No decorator support is needed on your side at all.

Using the class-based plugin API does require Stage 3 decorators in your project. If you write custom operators using @operator, @terminal, @orderedOperator, or @cachedOperator, your tsconfig must not have experimentalDecorators: true, because TypeScript supports only one decorator model at a time. If your project already uses the legacy model, the functional plugin API is fully equivalent and has no such constraint.

How the dist handles decorators

The published dist is compiled by tsup (esbuild) targeting es2017. esbuild transforms Stage 3 decorator syntax into plain helper functions (__decorateClass, __decorateElement) at build time, so the output works in every Node version, bundler, and consumer project - including those with no native decorator support at all.

| Aspect | Detail | |---|---| | Raw @decorator syntax in dist | No - compiled to helper functions | | Node 18+ | Works | | Projects with experimentalDecorators: true | Works (built-ins and functional plugin API) | | Extra bundle overhead | Small - one shared set of helpers per entry point |

Trade-offs to be aware of:

  • Helper-function output is slightly larger than native decorator syntax would be. The helpers are small and shared across all operators within an entry point, so in practice the impact is minimal.
  • Tools that understand Stage 3 decorator syntax natively (newer runtimes, bundlers) do not get any advantage from the raw syntax - they just run the helper functions instead.
  • This is the correct approach for a published package right now. Shipping raw @decorator syntax would silently break consumers on Node versions or bundlers that do not handle it.

Future: When TC39 Stage 3 decorators are universally supported in all target runtimes and bundlers, the build toolchain will be updated to pass decorator syntax through natively, removing the helper-function overhead. Until that point, the helper-based output is the safe and correct choice.

Compatibility

All projects: Built-in operators and sequences work without any decorator support. Import and use normally.

Projects with experimentalDecorators: true (NestJS, Angular, InversifyJS): The built-in operators and the functional plugin API (createGeneratorOperator, createOperator, and related helpers) work without any changes. The class-based decorator plugin API (@operator, @terminal, and related decorators) cannot be used because TypeScript supports only one decorator model at a time. Use the functional API instead - it is fully equivalent and the recommended path in these projects.

Projects with TypeScript 5.0+ and no experimentalDecorators: Full access to everything, including the class-based plugin API decorators.


Quick tour

import { Tyneq } from "tyneq";

// Wrap any iterable
Tyneq.from([1, 2, 3]);
Tyneq.from(new Set(["a", "b"]));
Tyneq.range(1, 5);    // [1, 2, 3, 4, 5]
Tyneq.empty<number>();

// Compose operators - nothing runs yet
const query = Tyneq.range(1, 1_000_000)
  .where((n) => n % 2 === 0)
  .select((n) => n * n)
  .take(5);

// Execute with a terminal
query.toArray();  // [4, 16, 36, 64, 100]
query.count();    // 5 - same query, independent traversal
query.first();    // 4

// Standard iteration works too
for (const n of query) console.log(n);
const arr = [...query];

Multi-key sorting, grouping, joins - all built in:

Tyneq.from(employees)
  .where((e) => e.department === "engineering")
  .orderBy((e) => e.level)
  .thenByDescending((e) => e.yearsAtCompany)
  .groupBy(
    (e) => e.team,
    (e) => e.name,
    (team, members) => ({ team, members: members.toArray() })
  )
  .toArray();

Operators

80+ operators across three categories.

Streaming (O(1) memory)

select where take takeWhile takeUntil skip skipWhile skipLast skipUntil slice selectMany flatten append prepend concat zip scan pairwise window chunk split repeat defaultIfEmpty populate ofType tap tapIf throttle pipe

Buffering (reads full source once)

orderBy orderByDescending thenBy thenByDescending groupBy distinct distinctBy reverse shuffle union unionBy intersect intersectBy except exceptBy join groupJoin backsert memoize permutations

Terminal (executes the pipeline)

toArray toSet toMap toRecord toAsync first firstOrDefault last lastOrDefault single singleOrDefault elementAt elementAtOrDefault count countBy sum average min max minBy maxBy minMax aggregate any all contains indexOf sequenceEqual startsWith endsWith isNullOrEmpty consume


Query plan

Every sequence carries a query plan tree. Access it, print it, walk it, transform it, or compile it back into an executable sequence.

import { QueryPlanPrinter, tyneqQueryNode } from "tyneq";

const seq = Tyneq.from([1, 2, 3])
  .where((x) => x > 1)
  .select((x) => x * 2)
  .take(5);

console.log(QueryPlanPrinter.print(seq[tyneqQueryNode]!));
// from([1, 2, 3])
//   -> where(<fn>)
//   -> select(<fn>)
//   -> take(5)

The QueryPlanCompiler takes any plan node and produces a fully executable sequence. Pass a source option to run the same pipeline against different data without rebuilding it:

const plan = Tyneq.from(data).where((x) => x > 0).select((x) => x * 2)[tyneqQueryNode]!;
const compiler = new QueryPlanCompiler();

compiler.compile(plan, { source: datasetA }).toArray();
compiler.compile(plan, { source: datasetB }).toArray();

Store pipelines as metadata, optimize them, replay them on any source. See the Query Plan guide for the full picture.


Docs

chrisitopherus.github.io/tyneq

| | | |---|---| | Getting Started | Install, first query, sources, re-iteration | | Core Concepts | Execution model, streaming vs. buffering, memoization | | Operators | All 80+ operators with examples | | Custom Operators | Functional API and decorators | | Plugin Internals | Registry, custom enumerators, utility helpers | | Query Plan | Plan access, printing, walking, transforming, compiling | | Best Practices | Patterns, pitfalls, and performance guidance | | API Reference | Full generated API docs |


Stability

Tyneq follows Semantic Versioning. The public API contract covers:

  • All methods on TyneqSequence, TyneqOrderedSequence, and TyneqCachedSequence
  • All symbols exported from the tyneq, tyneq/plugin, and tyneq/utility subpaths
  • The Tyneq static factory class

Internal classes (TyneqEnumerableBase, TyneqEnumerableCore, and anything tagged @internal) are not part of the contract and may change between minor versions.


Contributing

git clone https://github.com/chrisitopherus/tyneq
npm install
npm run build    # compile CJS + ESM + types
npm test         # run test suite
npm run lint     # check style
npm run docs:dev # local docs site

See the Contributing guide for the full workflow, including how to add operators, run the test suite, and submit a PR.

Bug reports and feature requests: github.com/chrisitopherus/tyneq/issues


License

MIT Copyright (c) 2026 chrisitopherus