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

@thi.ng/paths

v5.2.29

Published

Immutable, optimized and optionally typed path-based object property / array accessors with structural sharing

Readme

@thi.ng/paths

npm version npm downloads Mastodon Follow

[!NOTE] This is one of 211 standalone projects, maintained as part of the @thi.ng/umbrella monorepo and anti-framework.

🚀 Please help me to work full-time on these projects by sponsoring me on GitHub. Thank you! ❤️

About

Immutable, optimized and optionally typed path-based object property / array accessors with structural sharing.

Status

STABLE - used in production

Search or submit any issues for this package

Breaking changes

4.0.0

Naming convention

As part of a larger effort to enforce more consistent naming conventions across various umbrella packages, all higher-order operators in this package are now using the def prefix: e.g. getterT() => defGetter(), setterT() => defSetter().

Type checked accessors

Type checked accessors are now the default and those functions expect paths provided as tuples. To continue using string based paths (e.g. "a.b.c"), alternative Unsafe versions are provided. E.g. getIn() (type checked) vs. getInUnsafe() (unchecked). Higher-order versions also provide fallbacks (e.g. getter() => defGetterUnsafe()).

Type checking for paths is currently "only" supported for the first 8 levels of nesting. Deeper paths are supported but only partially checked and their value type inferred as any.

Related packages

  • @thi.ng/atom - Mutable wrappers for nested immutable values with optional undo/redo history and transaction support

Installation

yarn add @thi.ng/paths

ESM import:

import * as paths from "@thi.ng/paths";

Browser ESM import:

<script type="module" src="https://esm.run/@thi.ng/paths"></script>

JSDelivr documentation

For Node.js REPL:

const paths = await import("@thi.ng/paths");

Package sizes (brotli'd, pre-treeshake): ESM: 1.13 KB

Dependencies

Note: @thi.ng/api is in most cases a type-only import (not used at runtime)

Usage examples

Eight projects in this repo's /examples directory are using this package:

| Screenshot | Description | Live demo | Source | |:--------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------|:----------------------------------------------------------|:---------------------------------------------------------------------------------------| | | Using hdom in an Elm-like manner | Demo | Source | | | UI component w/ local state stored in hdom context | Demo | Source | | | Example for themed components proposal | Demo | Source | | | Event handling w/ interceptors and side effects | Demo | Source | | | Minimal demo of using rstream constructs to form an interceptor-style event loop | Demo | Source | | | Declarative component-based system with central rstream-based pubsub event bus | Demo | Source | | | Obligatory to-do list example with undo/redo | Demo | Source | | | Triple store query results & sortable table | Demo | Source |

API

Generated API docs

Type checked paths

As stated in the breaking changes section, since v4.0.0 paths are now type checked by default. These new functions use Typescript generics to validate a given path against the type structure of the target state object. Since string paths cannot be checked, only path tuples are supported. Type checking & inference supports path lengths up to 8 (i.e. levels of hierarchy) before reverting back to any for longer/deeper paths (there's no depth limit per se).

Due to missing type information of the not-yet-known state value, using the typed checked higher-order versions (e.g. defGetter, defSetter etc.) is slightly more verbose compared to their immediate use, first-order versions (e.g. getIn(), setIn() etc.), where everything can be inferred directly. However, (re)using the HOF-constructed accessors can be somewhat faster and more convenient... YMMV! More details below.

Optional property handling

When accessing data structures with optional properties, not only the leaf value type targeted by a lookup path is important, but any intermediate optional properties need to be considered too. Furthermore, we need to distinguish between read (get) and write (update) use cases for correct type inference.

For example, given these types:

type Foo1 = { a: { b: { c?: number; } } };

type Foo2 = { a?: { b: { c: number; } } };

For get/read purposes the inferred type for c will both be number | undefined. Even though c in Foo2 is not marked as optional, the a property is optional and so attempting to lookup c can yield undefined...

For set/update/write purposes, the type for c is inferred verbatim. I.e. if a property is marked as optional, a setter will allow undefined as new value as well.

Higher-order accessors

The defGetter(), defSetter() and defUpdater() functions compile a lookup path tuple into an optimized function, operating directly at the value the path points to in a nested object given later. For getters, this essentially compiles to:

import { defGetter } from "@thi.ng/paths";

defGetter(["a","b","c"]) => (obj) => obj.a.b.c;

...with the important difference that the function returns undefined if any intermediate values along the lookup path are undefined (and doesn't throw an error).

For setters / updaters, the resulting function too accepts a single object (or array) to operate on and when called, immutably replaces the value at the given path, i.e. it produces a selective deep copy of obj up until given path. If any intermediate key is not present in the given object, it creates a plain empty object for that missing key and descends further along the path.

import { defGetter } from "@thi.ng/paths";

// define state structure (see above example)
interface State {
    a: {
        b?: number;
        c: string[];
    }
}

const state: State = { a: { b: 1, c: ["c1", "c2"] } };

// build type checked getter for `b` & `c`
const getB = defGetter<State, "a", "b">(["a", "b"]);
const getFirstC = defGetter<State, "a", "c", 0>(["a", "c", 0]);

const b = getB(state); // b inferred as `number | undefined`
const c1 = getFirstC(state); // c1 inferred as `string`

Paths can also be defined as dot-separated strings, however cannot be type checked and MUST use the Unsafe version of each operation:

import { defSetterUnsafe } from "@thi.ng/paths";

s = defSetterUnsafe("a.b.c");

s({ a: { b: { c: 23 } } }, 24)
// { a: { b: { c: 24 } } }

s({ x: 23 }, 24)
// { x: 23, a: { b: { c: 24 } } }

s(null, 24)
// { a: { b: { c: 24 } } }

Nested value updaters follow a similar pattern, but also take a user supplied function to apply to the existing value (incl. any other arguments passed):

import { defUpdater } from "@thi.ng/paths";

type State = { a?: { b?: number; } };

const incAB = defUpdater<State, "a", "b">(
    ["a","b"],
    // x inferred as number | undefined
    (x) => x !== undefined ? x + 1 : 1
);

incAB({ a: { b: 10 } });
// { a: { b: 11 } }

incAB({});
// { a: { b: 1 } }

// with additional arguments
const add = defUpdater("a.b", (x, n) => x + n);

add({ a: { b: 10 } }, 13);
// { a: { b: 23 } }

First order operators

In addition to these higher-order functions, the module also provides immediate-use wrappers: getIn(), setIn(), updateIn() and deleteIn(). These functions are using defGetter / defSetter internally, so come with the same contracts/disclaimers...

import { deleteIn, getIn, setIn, updateIn } from "@thi.ng/paths";

const state = { a: { b: { c: 23 } } };

const cPath = <const>["a", "b", "c"];

getIn(state, cPath)
// 23

setIn(state, cPath, 24)
// { a: { b: { c: 24 } } }

// apply given function to path value
// Note: New `c` is 24, since above `setIn()` didn't mutate orig
updateIn(state, cPath, (x) => x + 1)
// { a: { b: { c: 24 } } }

// immutably remove path key
deleteIn(state, cPath)
// { a: { b: {} } }

Deletions

Since deleteIn immutably removes a key from the given state object, it also returns a new type from which the key has been explicitly removed. Those return types come in the form of Without{1-8}<...> interfaces.

import { deleteIn } from "@thi.ng/paths";

// again using `state` from above example
// remove nested key `a.c`
const state2 = deleteIn(state, ["a","b","c"]);

// compile error: "Property `c` does not exist`
state2.a.b.c;

Prototype pollution

Mainly a potential concern for the non-typechecked versions - currently, only the mutation functions (i.e. mutIn, mutInUnsafe(), deleteIn, deleteInUnsafe etc.) explicitly disallow updating an object's __proto__, prototype or constructor properties. The package provides the disallowProtoPath() helper which can be used in conjunction with the other setters in situations where it's advisable to do so.

import { disallowProtoPath, muIn, setIn } from "@thi.ng/paths";

// always checked for pollution
mutIn({}, ["__proto__", "polluted"], true);
// Uncaught Error: illegal argument(s): illegal path: ["__proto__","polluted"]

// manually checked
setIn({}, disallowProtoPath("__proto__.polluted"), true);
// Uncaught Error: illegal argument(s): illegal path: "__proto__.polluted"

Structural sharing

Only keys in the path will be updated, all other keys present in the given object retain their original/identical values to provide efficient structural sharing / re-use. This is the same behavior as in Clojure's immutable maps or those provided by ImmutableJS (albeit those implementation are completely different - they're using trees, we're using the ES6 spread op (for objects, slice() for arrays) and dynamic functional composition to produce the setter/updater).

import { defSetterUnsafe } from "@thi.ng/paths";

const s = defSetterUnsafe("a.b.c");

// original
const a = { x: { y: { z: 1 } }, u: { v: 2 } };
// updated version
const b = s(a, 3);
// { x: { y: { z: 1 } }, u: { v: 2 }, a: { b: { c: 3 } } }

// verify anything under keys `x` & `u` is still identical
a.x === b.x // true
a.x.y === b.x.y // true
a.u === b.u; // true

Mutable setter

defMutator()/defMutatorUnsafe() are the mutable alternatives to defSetter()/defSetterUnsafe(). Each returns a function, which when called, mutates given object / array at given path location and bails if any intermediate path values are non-indexable (only the very last path element can be missing in the actual target object structure). If successful, returns original (mutated) object, else undefined. This function too provides optimized versions for path lengths <= 4.

As with setIn, mutIn is the immediate use mutator, i.e. the same as: defMutator(path)(state, val).

import { mutIn, mutInUnsafe } from "@thi.ng/paths";

mutIn({ a: { b: [10, 20] } }, ["a", "b", 1], 23);
// or
mutInUnsafe({ a: { b: [10, 20] } }, "a.b.1", 23);
// { a: { b: [ 10, 23 ] } }

// no-op (because of missing path structure in target object)
mutInUnsafe({}, "a.b.c", 23);
// undefined

Path checking

The exists() function takes an arbitrary object and lookup path (string or tuple). Descends into object along path and returns true if the full path exists (even if final leaf value is null or undefined). Checks are performed using hasOwnProperty().

import { exists } from "@thi.ng/paths";

exists({ a: { b: { c: [null] } } }, "a.b.c.0");
// true

exists({ a: { b: { c: [null] } } }, ["a", "b", "c", 1]);
// false

Authors

If this project contributes to an academic publication, please cite it as:

@misc{thing-paths,
  title = "@thi.ng/paths",
  author = "Karsten Schmidt",
  note = "https://thi.ng/paths",
  year = 2016
}

License

© 2016 - 2025 Karsten Schmidt // Apache License 2.0