# tidy.js: dplyr-style data wrangling for JavaScript

> tidy.js is a TypeScript library that brings the grammar of the R tidyverse to JavaScript, with a tidy() pipeline that takes an array of plain objects and a sequence of transformation verbs. It has no wrapper classes, it includes grouped operations with several export shapes, and it ships an API reference, a playground and Observable notebooks.

**pbeshai/tidy** — Tidy up your data with JavaScript, inspired by dplyr and the tidyverse

- Repository: https://github.com/pbeshai/tidy
- Website: https://pbeshai.github.io/tidy
- Stars: 761 · Forks: 21
- Language: TypeScript
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/pbeshai-tidy

## The three goals and the plain object decision

tidy.js attempts to bring the ergonomics of data manipulation from R to JavaScript and TypeScript, and it states three primary goals.

The first is readable code. The project prioritises making data transformations readable so that future you and your teammates can get up and running quickly.

The second is standard transformation verbs, built using battle-tested verbs from the R community that can handle any data wrangling need.

The third is the one that shapes the API. It works with plain JavaScript objects, with no wrapper classes needed. All tidy.js needs is an array of plain old-fashioned JavaScript objects to get started, simple in and simple out.

That last decision is the library's defining trade. A data frame library wraps your rows in objects that carry metadata about the columns, which buys you column selection, type checking and row-wise operations, and costs you an abstraction layer and a conversion at the boundary. tidy.js declines the wrapper, so the input is an array of objects and the output is an array of objects, and everything the library offers is expressed as a function over that shape.

There is a secondary goal the README states plainly: the project also aims to provide acceptable types for the functions it provides, which is the TypeScript half of the proposition.

The licence field on the repository says MIT, the primary language is TypeScript, and the homepage is the project site at pbeshai.github.io/tidy.

## Installing it and the first pipeline

The package is on npm as @tidyjs/tidy.

```shell
npm install @tidyjs/tidy
# or
yarn add @tidyjs/tidy
```

You import the verbs you need.

```js
import { tidy, mutate, arrange, desc } from '@tidyjs/tidy'
```

And you apply them to an array of objects.

```js
const data = [
  { a: 1, b: 10 },
  { a: 3, b: 12 },
  { a: 2, b: 10 }
]

const results = tidy(
  data,
  mutate({ ab: d => d.a * d.b }),
  arrange(desc('ab'))
)
```

The output is the same array of objects with the new column added and the rows sorted, descending by it.

```js
[
  { a: 3, b: 12, ab: 36},
  { a: 2, b: 10, ab: 20},
  { a: 1, b: 10, ab: 10}
]
```

All of the code is wrapped in a tidy flow via the tidy() function. The first argument is the array of data, followed by the transformation verbs to run on it.

The important part is what the verbs must satisfy. The actual functions passed to tidy() can be anything so long as they fit the form of a function from an array of objects to an array of objects, which means you can write your own transformation and drop it into the pipeline, and the README gives the example of a plain filter passed alongside arrange().

That contract is the whole API. It is why the library composes with anything else that speaks the same shape.

## Grouped data and the export shapes

Beyond flat lists, tidy provides facilities for wrangling grouped data through the groupBy() function, which takes a flow of functions as its second argument wrapped in an array.

```js
import { tidy, summarize, sum, groupBy } from '@tidyjs/tidy'

const data = [
  { key: 'group1', value: 10 },
  { key: 'group2', value: 9 },
  { key: 'group1', value: 7 }
]

tidy(
  data,
  groupBy('key', [
    summarize({ total: sum('value') })
  ])
)
```

The output is one summarised object per group.

```js
[
  { "key": "group1", "total": 17 },
  { "key": "group2", "total": 9 },
]
```

The third argument of groupBy is the interesting part, because it changes the shape of the export rather than the computation. The README describes it as exporting the grouped data into different shapes, and the example builds a nested object keyed by two grouping columns.

```js
const data = [
  { g: 'a', h: 'x', value: 5 },
  { g: 'a', h: 'y', value: 15 },
  { g: 'b', h: 'x', value: 10 },
  { g: 'b', h: 'x', value: 20 },
  { g: 'b', h: 'y', value: 30 },
]

tidy(
  data,
  groupBy(
    ['g', 'h'],
    [
      mutate({ key: d => `\${d.g}\${d.h}`})
    ],
    groupBy.object()
  )
);
```

The result is a nested object with an array of rows at each leaf.

```js
{
  "a": {
    "x": [{"g": "a", "h": "x", "value": 5, "key": "ax"}],
    "y": [{"g": "a", "h": "y", "value": 15, "key": "ay"}]
  },
  "b": {
    "x": [
      {"g": "b", "h": "x", "value": 10, "key": "bx"},
      {"g": "b", "h": "x", "value": 20, "key": "bx"}
    ],
    "y": [{"g": "b", "h": "y", "value": 30, "key": "by"}]
  }
}
```

The alternative export produces key and values entries instead, using groupBy.entriesObject().

The list of available exports is longer than that, and the README gives it as a quick reference: groupBy.entries(), groupBy.entriesObject(), groupBy.grouped(), groupBy.levels(), groupBy.object(), groupBy.keys(), groupBy.map() and groupBy.values(). The one it singles out is levels, which can let you mix and match different export types based on the depth of the data.

## The single leaf option and the rest of the toolchain

One export option is worth a separate mention because it addresses a real annoyance.

It is common to be left with a single leaf in a groupBy set, especially after running summarize(). To prevent exported data having its values wrapped in an array, you can pass the single option.

```js
tidy(input,
  groupBy(['g', 'h'], [
    summarize({ total: sum('value') })
  ], groupBy.object({ single: true }))
);
```

The output is then a plain value at each leaf rather than an array containing one value.

```js
{
  "a": {
    "x": {"total": 5, "g": "a", "h": "x"},
    "y": {"total": 15, "g": "a", "h": "y"}
  },
  "b": {
    "x": {"total": 30, "g": "b", "h": "x"},
    "y": {"total": 30, "g": "b", "h": "y"}
  }
}
```

The rest of the toolchain is a monorepo with npm workspaces. The root package.json declares the workspaces as packages/* and website, and the scripts build the tidy and tidy-moment packages individually, with separate scripts for the website, the tests, the linting and the formatting. The root package is marked private and the name is tidy-packages.

The build tooling is rollup with the commonjs and node-resolve plugins and an esbuild-based plugin, and the test runner is vitest. Linting is eslint with a flat config and prettier for formatting, and there is a husky setup with a commit-msg hook running commitlint and a pre-commit hook running lint-staged over the package sources.

There is a skills/ directory at the root, which is a convention borrowed from the agent tooling world, and the website/ directory holds the documentation site, the playground and the API reference. The README also links an Observable intro and an Observable examples collection, and a CodeSandbox showing basic HTML usage of the UMD build, which is a quick way to see the library working before installing it.

## Where tidy.js is the wrong choice

The first limitation is the deliberate absence of a table abstraction. There are no column types, no row selection by predicate, no schema, and the library does not track which columns exist. That is fine for an array of objects you control, and it is a real constraint if your data is heterogeneous, since a missing property is a runtime undefined rather than a caught error.

The second is that the evaluation is eager. Every verb returns a new array, and a pipeline of ten verbs on a large array allocates ten intermediate arrays. A real data frame library is lazy and columnar, and the difference is not visible on a few hundred rows and is visible on a few hundred thousand.

The third is the size of the vocabulary. The verbs are the dplyr set, which is broad, but the library is a JavaScript reimplementation of a specific set of semantics, and there are corners of R's data manipulation that it does not attempt. The README points to the API reference for what each function does and what options it takes, which is where you find out whether a particular operation is supported.

The fourth is the maturity of the project. The repository has no GitHub releases, the last push was on 2026-04-06, and the root package.json still carries a mix of tooling generations, with prettier at version 2 alongside eslint at 10, which is the kind of combination that suggests a repository that is maintained but not modernised as a whole.

The fifth is that there is a closer alternative in the same space, which the README itself names. It points to Arquero from UW Data as a very similar project, and Arquero is a columnar table library with its own verbs. A team choosing between the two is choosing between a plain-object approach and a typed table approach, and that decision is more consequential than the verb names.

## How tidy.js compares with Arquero and with plain array methods

The comparison with Arquero is the one the README raises, and the difference in approach is the shape of the data. tidy.js operates on arrays of plain objects and never wraps them, so your data goes in and comes out in the same form, and every verb is a function over that shape. Arquero is a columnar table, with typed columns, a query plan and column operations that do not walk rows.

That difference decides who each is for. tidy.js is for a codebase where the data arrives as JSON from an API and the transformations are ad hoc. Arquero is for analysis over a data set you have loaded and want to query repeatedly.

The second comparison is with plain array methods, and it is the one that matters most because the array methods are already there and free. A pipeline of filter, map and sort is three lines of JavaScript. The argument for tidy.js is not that it does more, it is that the verbs name intentions, that a reviewer can read the pipeline and see what transformation is happening, and that the same names mean the same thing as in the R code next door.

That argument is real, and it is stronger in a team than in a project, because the readability is for the person who did not write the line.

The third comparison is with lodash. Lodash is a general utility library and tidy.js is a data library, and the overlap is smaller than it looks. Lodash gives you map, filter and groupBy over objects and arrays. tidy.js gives you a data grammar with grouped operations and export shapes, and it gives you names that come from a coherent model of data manipulation rather than a collection of helpers.

The fourth comparison is with a dataframe library for JavaScript, which is where the wrapper cost comes back, and where you decide whether the type safety is worth it.

## Licence, project state and what to check before you adopt it

The licence field on the repository says MIT, and the root package.json declares the same, with a LICENSE file at the root. A permissive licence with no copyleft obligation is the right choice for a library that a large number of projects will depend on.

The project is not archived and the last push was on 2026-04-06, so it is maintained. The package is published on npm as @tidyjs/tidy, and the repository has no GitHub releases, so the npm version is the version to pin.

The repository layout is a workspace monorepo. There is a packages/ directory holding the published packages, including the tidy package and a tidy-moment package, a website/ directory for the documentation site and the playground, a skills/ directory, a .circleci/ directory for the build, a CHANGELOG.md, an eslint config, a prettier config, and a package-lock.json alongside the workspaces definition.

Four things to check before you adopt it.

Whether the verbs you need exist, because the API reference is the place to find out and the coverage is a reimplementation of dplyr rather than of all of R's data tooling.

Whether the plain object approach fits your data, in particular whether your records are uniform enough that a missing property is not a realistic case.

What the grouped operations do to your output shape, since the export argument is what determines whether you get an array of rows, a nested object or a key and values structure, and the levels export is the one to read about if your data varies in depth.

And the type situation, because the README describes acceptable types as a secondary goal, so the TypeScript definitions are worth looking at before you rely on them in a codebase with strict settings.

## Conclusion

tidy.js suits a JavaScript or TypeScript team that finds the array method chain hard to read and wants the transformations to look like a sequence of named intentions instead, and it suits a data frame user working in the browser. It is the wrong tool if your data already lives in a table abstraction, since the whole point is that there is none, and the wrong choice if you need the lazy evaluation and grouped semantics of a true data frame. Before adopting it, read the API reference for the verbs you need, and look at the levels export, which the README singles out as the way to mix export shapes by data depth.

## FAQ

### What is tidy.js?

It is a TypeScript library that brings the data manipulation grammar of dplyr and the tidyverse to JavaScript. It takes an array of plain JavaScript objects through a pipeline of named transformation verbs, with no wrapper classes, and it also provides grouped operations with several export shapes.

### How do I install tidy.js and run a first transformation?

Install the package with `npm install @tidyjs/tidy` or `yarn add @tidyjs/tidy`, import the verbs you need, and call tidy() with your array followed by the verbs. A UMD build is hosted on jsdelivr for trying it in a browser without a build step.

### How does groupBy in tidy.js export its results?

The third argument of groupBy chooses the export shape. groupBy.object() produces a nested object, groupBy.entriesObject() produces key and values entries, and other options include groupBy.entries(), groupBy.grouped(), groupBy.levels(), groupBy.keys(), groupBy.map() and groupBy.values(). The levels export can mix export types based on the depth of the data.

### Can I write my own transformation for tidy.js?

Yes. The functions passed to tidy() can be anything that fits the form of a function from an array of objects to an array of objects, so a plain filter can be placed in the pipeline alongside the built-in verbs.

### What other project does the tidy.js README compare itself to?

It points to Arquero from UW Data as a very similar project, and the two differ in that tidy.js works on plain objects with no wrapper while Arquero is a columnar table with typed columns.

### What licence does tidy.js use?

MIT, with the terms in the LICENSE file at the root of the repository, and the root package.json declares the same licence.

## Sources

- [Issues](https://github.com/pbeshai/tidy/issues)
- [License: MIT](https://github.com/pbeshai/tidy/blob/main/LICENSE)
- [pbeshai/tidy on GitHub](https://github.com/pbeshai/tidy)
- [Project website](https://pbeshai.github.io/tidy)
- [README](https://github.com/pbeshai/tidy/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/pbeshai-tidy
