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 🙏

© 2025 – Pkg Stats / Ryan Hefner

object-recipes

v0.0.8

Published

A tiny javascript/typescript library for modifying large deeply-nested objects in a immutable, type-safe and composable manner.

Readme

object-recipes

This library provides a convenient and type-safe pattern to structure your code, by organizing updates into composable functions that can even be nested within eachother.

This can be useful when:

  • you have a state object and you want to structure updates in a composable way.
  • you have large deeply-nested objects with lots of actions/updates.

You can perform nested updates by specifying the entire nested-path as a string (arrays also supported!), and have it updated immutably. The string is type-checked!

It has a fast optional deep-equal comparator built-in, but uses Object.is by default. If an attempted change is equal to current values, simply return the current unchanged instance/object.

This library includes full typings for projects that use typescript. An object/entity is immutable, so making any changes will return a new object/entity.

Also a good alternative to immer, and much more lightweight!
Can be used in combination with:

object-recipes is tiny!: (Even this README is bigger than the library itself)
(minified at 2025/10/05)

  • index.es.js (es): 0.93 kB (gzip)
  • index.cjs.js (cjs): 0.86 kB (gzip)
  • index.iife.js (iife): 0.88 kB (gzip)

Installation

npm i object-recipes
yarn add object-recipes
bun add object-recipes

Basic usage

import { entity, Recipe, Shape } from 'object-recipes'

// Initialize the entity.
const person = entity({
  name: '',
  age: 0,
  address: { street: '', zip: 0, country: '' },
  activities: ['Skiing', 'Climbing', 'Skateboarding'],
});
// By default uses `Object.is` as a comparator to check
// if an attempted update is not equal to the current value.
// You can enable the built-in deepEqual or use your own
// by passing a config parameter:
// entity(someObject,
//   { deepEqual: true } // true = built-in deepEqual
// );
// `true` uses the built-in deepEqual, if you want to use
// a custom comparator-function you can simply pass that
// instead of `true`.

// Retrieve the plain js-object.
// Note: If you modify this object directly, it will
// break immutability. If you plan to modify it directly
// you should instead use getClone() below.
person.get();

// Or retrieve a deep-copy of the plain object.
// This way the object is not in any way connected
// to the entity, since it is a deep-copy.
person.getClone();

// Update one or more fields on the entity.
// set() returns a new entity-instance with updated fields,
// without touching the original instance.
//
// If new values (all of them) are identical (Object.is) to
// current values, there is no change happening, so the
// current instance is returned unchanged.
// The comparator uses the global comparator on this entity,
// but can be overridden by passing a second `deepEqual` argument
// to set: entity(..).set({ .. }, true);
//
// Argument is type-safe and will give errors if invalid.
const update = person.set({ name: 'John Doe', age: 20 }).get();

// You can chain multiple operations in a row before calling get()
person.set({ name: 'John Doe' }).set({ age: 20 }).get();

// Update a single field in a nested path.
// setPath() returns a new entity-instance with updated fields,
// without touching the original instance.
//
// If the new value is identical (Object.is) to current value, there
// is no change happening, so the current instance is returned
// unchanged.
// The comparator uses the global comparator on this entity,
// but can be overridden by passing a second `deepEqual` argument
// to setPath: entity(..).setPath('..', value, true);
//
// Both path and value are type-safe and will give errors if invalid.
const nestedUpdate = person.setPath('address.street', 'Teststreet 1').get();

// Update an array-item.
// Will change activities[0] from 'Skiing' to 'Downhill skiing'.
const arrayUpdate = person.setPath('activities[0]', 'Downhill skiing').get();

// Add new item to end of array.
// After the change, `activities` looks like this:
// ['Skiing', 'Climbing', 'Skateboarding', 'Fishing']
const arrayAddUpdate = person.setPath('activities[3]', 'Fishing').get();

// Does also work when the base object is an array.
const arrayEntity = entity([{ value: 'Hello' }]);
// Update value from `Hello` to `Hello sir!`
arrayEntity.setPath('[0].value', 'Hello sir!');
// set() will simply reset the entire entity to the new value, for arrays.
arrayEntity.set([{ value: 'Something' }]);

// Defining a recipe.
const addressRecipe = (
  street: string, zip: number, country: string
): Recipe<typeof person> => (entity) => entity.set(
  { address: { street, zip, country } }
);

// Using the recipe on an entity to perform an update.
const recipeUpdate = person.recipe(
  addressRecipe('Teststreet 1', 1000, 'Norway')
);

// Use the `Shape` type to infer the type/structure of the underlying object.
// Result:
// {
//   name: string, age: number,
//   address: { street: string, zip: number, country: string },
//   activities: string[],
// }
type RealObject = Shape<typeof person>;

Advanced usage

// With these basic tools you can keep expanding by using recipes inside
// recipes, this is where the real power lies

const testAddressRecipe = (): Recipe<typeof person> => (entity) =>
  entity.recipe(addressRecipe('Teststreet 1', 1000, 'Norway'));

const testNameAndAgeRecipe = (): Recipe<typeof person> => (entity) =>
  entity.set({ name: 'John Doe', age: 20 });

const testPersonRecipe = (): Recipe<typeof person> => (entity) => 
  entity.recipe(testAddressRecipe()).recipe(testNameAndAgeRecipe());

const testResult = person.recipe(testPersonRecipe()) // Finally we run all the recipes
    .get(); // and retrieve the resulting object 

// You can also chain set() and recipe() operations 

person.set({ name: 'John Doe' }).set({ age: 20 })
  .recipe(testAddressRecipe()).get();

// Using recipes from a "parent-entity" onto a "child-entity" that inherited/extended the base-entity. 
// In order to achieve this the recipe-function must be made generic.
// We need to do some assertions inside the generic function, in order for set/setPath arguments
// to be type-safe.

const genericTestAddressRecipe = <T extends typeof person>(): Recipe<T> => (entity) =>
  (
    (entity as typeof person)
      .set({
        address: { street: 'Teststreet 1', zip: 1000, country: 'Norway' }
      })
  ) as typeof entity;

const genericStreetRecipe = <T extends typeof person>(): Recipe<T> => (entity) =>
  (
    (entity as typeof person)
      .setPath('address.street', 'Teststreet 1')
  ) as typeof entity;

const employee = entity({
  ...person.get(),
  jobTitle: '',
  salary: 0
});

const update = employee.recipe(genericTestAddressRecipe()).recipe(genericStreetRecipe());

// That's it!
// You now have the power to create bigger entities with associated recipes.
//
// You can even create sub-entities with their own sub-recipes, and then you
// call get() on the sub-entity and set() it back into the parent entity.