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

single-platter

v1.0.0

Published

Generic and safe boilerplate removal for operations on recursive data types

Readme

single-platter

Generic and safe boilerplate removal for operations on recursive data types. Based off of the Uniplate Haskell library.

Install

npm install --save single-platter

Motivation

Operations on recursive data types generally require defining one case per variant. For example, consider a recursive data type Expr for defining arithmetic expressions:

type Add = {
    tag: "Add";
    left: Expr;
    right: Expr;
};

type Sub = {
    tag: "Sub";
    left: Expr;
    right: Expr;
};

type Mul = {
    tag: "Mul";
    left: Expr;
    right: Expr;
};

type Div = {
    tag: "Div";
    left: Expr;
    right: Expr;
};

type Val = {
    tag: "Val";
    value: number;
};

type Neg = {
    tag: "Neg";
    inner: Expr;
};

export type Expr = Add | Sub | Mul | Div | Val | Neg;

Say we have an Expr and we want to simplify it by removing all subtraction operations, replacing them with the addition of a negation operation. So this:

{
    tag: "Sub",
    left: x,
    right: y
}

should turn into this:

{
    tag: "Add",
    left: x,
    right: {
        tag: "Neg",
        inner: y
    }
}

To do this normally, we need to define a recursive function which operates over every node in the tree. Here's the naive implementation:

function removeSubExplicit(expr: WithoutLet.Expr): WithoutLet.Expr {
    if (expr.tag === "Add")
        return {
            tag: "Add",
            left: removeSubExplicit(expr.left),
            right: removeSubExplicit(expr.right),
        };
    if (expr.tag === "Sub")
        return {
            tag: "Add",
            left: removeSubExplicit(expr.left),
            right: {
                tag: "Neg",
                inner: removeSubExplicit(expr.right),
            },
        };
    if (expr.tag === "Mul")
        return {
            tag: "Mul",
            left: removeSubExplicit(expr.left),
            right: removeSubExplicit(expr.right),
        };
    if (expr.tag === "Div")
        return {
            tag: "Div",
            left: removeSubExplicit(expr.left),
            right: removeSubExplicit(expr.right),
        };
    if (expr.tag === "Val") return expr;

    return {
        tag: "Neg",
        inner: removeSubExplicit(expr.inner),
    };
}

Here we've defined six cases, only one of which is "interesting". The more variants in our recursive data structure, the worse this uninteresting boilerplate becomes.

Here is how the same operation is defined with single-platter:

function removeSub(expr: Expr): Expr {
    return plate.transform(expr, (expr) =>
        expr.tag === "Sub"
            ? {
                  tag: "Add",
                  id: expr.id,
                  left: expr.left,
                  right: {
                      tag: "Neg",
                      id: "",
                      inner: expr.right,
                  },
              }
            : expr,
    );
}

This function definition is less than half as many lines. Even better, this line reduction makes the logic easier to understand by highlighting the relevant transformation and omitting irrelevant distractions. Even better, this definition is stack-safe, as none of the operations in this library use recursion internally. The naive implementation will crash on sufficiently-large inputs.

single-platter provides seven highly-flexible generic operations which can be used to make a broad range of transformations simpler and safer.

Usage

This library supports both ESM and CommonJS imports.

The sole export of this library is the Plate class. Plate must be instantiated with functions for getting all of the children of a recursive node, and for creating a new node from an existing node and a new set of children. For example, using our Expr language above:

import { Plate } from "single-platter";

function getChildren(expr: Expr): Array<Expr> {
    if (expr.tag === "Add") return [expr.left, expr.right];
    if (expr.tag === "Sub") return [expr.left, expr.right];
    if (expr.tag === "Mul") return [expr.left, expr.right];
    if (expr.tag === "Div") return [expr.left, expr.right];
    if (expr.tag === "Val") return [];
    return [expr.inner];
}

function fromChildren(expr: Expr, children: Array<Expr>): Expr {
    if (expr.tag === "Add")
        return {
            tag: "Add",
            left: children[0]!,
            right: children[1]!,
        };
    if (expr.tag === "Sub")
        return {
            tag: "Sub",
            left: children[0]!,
            right: children[1]!,
        };
    if (expr.tag === "Mul")
        return {
            tag: "Mul",
            left: children[0]!,
            right: children[1]!,
        };
    if (expr.tag === "Div")
        return {
            tag: "Div",
            left: children[0]!,
            right: children[1]!,
        };
    if (expr.tag === "Val")
        return {
            tag: "Val",
            value: expr.value,
        };
    return {
        tag: "Neg",
        inner: children[0]!,
    };
}

const plate = new Plate(getChildren, fromChildren);

The fromChildren implementation uses non-null assertions when accessing the children array. Users must ensure that, for every variant in their data type, getChildren(node) returns an array of the same length that fromChildren uses to create that variant. This is not enforced (and not practically enforceable) by the type system.

API

Documentation for Plate's methods lives separately:

Limitations

Monadic Uniplate operations have been omitted

The Haskell Uniplate library provides monadic versions of descend, rewrite, and transform. TypeScript's type system doesn't make representing these operations ergonomic. If you need an effectful transformation it is recommended that you pass impure functions to transformation methods.

For example, if you wish to annotate every node in a tree with a unique ID, you should use transform, passing in a function which uses an impure getFreshID operation.

Mutually recursive data types are not supported

This library only supports recursion over a single data type. As a point of comparison, many programming languages are defined via mutually recursive types, such as Statement and Expression types which each reference each other. The original Haskell Uniplate library handles this via the Biplate typeclass. Biplate is more involved than single-type Uniplate and is not implemented in this library.

I would be open to PRs to change this!

Contributing

Please see CONTRIBUTING.md.

Contributions and interactions made by or with the assistance of agentic or generative AI are not welcome and will result in an immediate ban from the project.

Some desired features are listed in WISHLIST.md.

License

MIT

Citations

This project is heavily based on the contents of a research paper:

Neil Mitchell and Colin Runciman. 2007. Uniform boilerplate and list processing. In Proceedings of the ACM SIGPLAN workshop on Haskell workshop (Haskell '07). Association for Computing Machinery, New York, NY, USA, 49–60. https://doi.org/10.1145/1291201.1291208