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 🙏

© 2024 – Pkg Stats / Ryan Hefner

evolve-ts

v2.2.0

Published

Immutably update nested objects with patches containing values or functions to update values

Downloads

3,387

Readme

evolve-ts

npm install evolve-ts

Immutably update nested objects with patches containing values or functions to update values

  • Simple yet powerful: simple syntax for performing immutable updates
  • Type-safe: Robust type checking and type inferences
  • Tiny: < 1kb gzipped, zero dependencies

Usage

Let's say we have the following state

import { evolve, unset } from "evolve-ts"

const state = {
    user: {
        name: "Alice",
        age: 22
    }
}

We can set a value

evolve({ user: { name: "Bob" } }, state)
// { user: { name: "Bob", age: 22 } }

Update a value

evolve({ user: { age: age => age + 1 } }, state)
// { user: { name: "Alice", age: 23 } }

Remove a key

evolve({ user: { age: unset } }, state)
// { user: { name: "Alice" } }

All together now!

import { evolve, unset } from "evolve-ts"

const alice = {
    name: "Alice",
    age: 22,
    active: true,
    likes: ["wonderland"],
    nested: {
        foo: true,
        bar: true,
    }
}

evolve({
    name: "Bob", // sets the value of name
    age: age => age + 1, // updates the value of age
    active: unset, // removes the active key
    likes: ["building"], // sets the value of likes (only objects are merged)
    nested: {
        foo: false // sets the value of foo
    }
}, alice)
/*
{
    name: "Bob",
    age: 23,
    likes: ["building"],
    nested1: {
        foo: false,
        bar: true,
    }
}
*/

Setting object values directly

Object values are merged recursively, but return values of functions are set directly. To replace an object value use a constant function.

evolve({
    user: () => ({
        name: "Bob",
        age: 33,
    })
}, state)

// { user: { name: "Bob", age: 33 } }

Removing object entries

Entries can be removed by setting the value to unset or using an updater function that returns unset.

evolve({
    user: {
        name: unset,
        age: () => unset
    }
}, state)

// { user: {} }

Working with Arrays

evolve-ts provides two functions to update arrays, map and adjust.

map is a version of the map function that can either take a callback or a patch. The following are all equivalent, they all increment the number of likes for each user in the users array.

import { map } from "evolve-ts"
const inc = n => n + 1

map({ likes: inc })(users)

map(evolve({ likes: inc }))(users)

map(user => ({ ...user, likes: inc }))(users)

adjust conditionally maps values with a callback function or patch. Value(s) to map can be specified with an index or predicate function. Negative indexes are treated as offsets from the array length.

import { adjust } from "evolve-ts"

// set the first user as preferred
adjust(0, { preferred: true })(users)

// set the last user as preferred
adjust(-1, { preferred: true })(users)

// set all users who have 100 or more likes as preferred
adjust(user => user.likes > 99, { preferred: true })(users)

// like map, can also take a callback to update the value
adjust(0, user => ({ ...user, preferred: true }))(users)

Other array helpers such as append and filter are not re-implemented by evolve-ts as they are already included in fp-ts, Ramda, and many other libraries.

import { adjust, evolve, map } from "evolve-ts"
import { append, filter, inc } from "" // your favorite functional utility library


const state = {
    users: [
        { name: "Alice", age: 22, id: 0 },
        { name: "Bob", age: 33, id: 1 }
    ]
}

// add a new user
evolve({
    users: append({ name: "Claire", age: 44, id: 2 })
}, state)

// increment the ages of all users
evolve({
    users: map({ age: inc })
}, state)

// set the age of a user by id
evolve({
    users: adjust(user => user.id === 1, { age: 55 })
}, state)

// remove a user by id
evolve({
    users: filter(user => user.id !== 1)
}, state)

Currying

import { evolve } from "evolve-ts"

const incrementAge = evolve({ age: age => age + 1 })

incrementAge({ name: "Alice", age: 22 })
// { name: "Alice", age: 23 }

TypeScript Support

The evolve function is strictly typed and does not allow polymorphism. The return type is always the same as the target type.

import { evolve, unset } from "evolve-ts"

// cannot change the type of values in target
evolve({ age: "22" }, { name: "Alice", age: 22 })
// TypeError: Type 'string' is not assignable to type 'number | ((value: number) => number)'

// updaters have correctly inferred types (updater argument in this example is inferred as number)
evolve({ age: age => age + 1 }, { name: "Alice", age: 22 })
// ReturnType: { name: string; age: number; }

// unset can be used on optional keys
evolve({ age: unset }, { name: "Alice", age: 22 } as { name: string; age?: number; })
// ReturnType: { name: string; age?: number; }

// unset cannot be used on required keys
evolve({ age: unset }, { name: "Alice", age: 22 })
// TypeError: Type 'typeof Unset' is not assignable to type 'number | ((value: number) => number)'

// cannot add extraneous properties to the patch
evolve({ name: "Alice", age: 23 }, { age: 22 })
// TypeError: ...'name' does not exist in type 'Patch<{ age: number; }>'

// cannot set non-nullable properties to undefined when exactOptionalPropertyTypes is enabled
evolve({ name: undefined }, { name: "Alice" })
// Type 'undefined' is not assignable to type 'string | ((value: string) => string)'

The evolve_ function is a type alias for evolve that allows polymorphism while still producing strongly typed results.

import { evolve_, unset } from "evolve-ts"

// changing age from number to string
evolve_({ age: "22" }, { name: "Alice", age: 22 })
// ReturnType: { name: string; age: string; }

// adding name key
evolve_({ name: "Alice", age: 23 }, { age: 22 })
// ReturnType: { name: string, age: number; }

// adding age key with updater function
evolve_({ age: (): number => 22 }, { name: "Alice" })
// ReturnType: { name: string, age: number; }

// removing age key
evolve_({ age: unset }, { name: "Alice", age: 22 })
// ReturnType: { name: string; }

Shallow Updates

The behavior of shallowEvolve is similar to the spread operator except it accepts updater functions.

import { evolve, shallowEvolve } from "evolve-ts"

declare const user

shallowEvolve({ user }) /* equivalent to */ evolve({ user: () => user })

// evolve can be used within shallowEvolve if a deep merge is needed for a particular key
shallowEvolve({ user: evolve(user) }) /* equivalent to */ evolve({ user })

Provided Functions

  • evolve: Takes a patch object and a target object and returns a version of the target object with updates from the patch applied. A patch is a subset of the target object containing either values or functions to update values. Functions are called with existing values from the target, non-object values are set into the target, and object values are merged recursively.
  • evolve_: Type alias for evolve that allows polymorphism while still producing strongly typed results.
  • map: Maps values in an array with a callback function or patch.
  • adjust: Conditionally maps values in an array with a callback function or patch. Value(s) to map can be specified with an index or predicate function. Negative indexes are treated as offsets from the array length.
  • unset: Sentinel value that causes its key to be removed from the output.
  • shallowEvolve: Like evolve but performs shallow updates.
  • shallowMap: Like map but performs shallow updates.
  • shallowAdjust: Like adjust but performs shallow updates.

Why evolve-ts?

evolve-ts was created as a lightweight alternative to updeep and immer. It has all of updeep's core functionality, strong TypeScript support, is only a fraction of the size of updeep or immer, and is dependency free.

Caveats

evolve-ts treats all functions in patches as updater functions, so if your state contains function values you want to update you must wrap the updates in a constant function.

import { evolve } from "evolve-ts"

const state = {
    name: "Alice",
    greet: (person) => console.log(`Hi ${person} I'm Alice!`)
}
// need to use a wrapper to set new function values
evolve({
    name: "Bob",
    greet: () => (person) => console.log(`Hi ${person} I'm Bob!`)
}, state)

License

MIT