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

inidot

v2.0.0

Published

Get, set, or delete a property from a nested object using a dot path

Readme

Inidot banner

Inidot :pencil:

Get, set, or delete a property from a nested object using a dot path :fire:

npmjs License Activity GitHub stars

Features

  • Lightweight 🪶 (zero dependencies)
  • Blazing fast 🔥 — memoized path parsing and linear-time flattening
  • Minimalist :white_circle: (but powerful)
  • 100% TypeScript :large_blue_diamond:
  • Suitable for large data :page_with_curl:
  • Prototype-pollution safe :lock: (__proto__, constructor, prototype paths are rejected)
  • Wildcard support :snowflake: — broadcast over arrays with *
  • Escaped dots :point_right: — literal dots inside keys via \.
  • Easy to use :bread:
  • ... and much more :rocket:

Install

<npm|pnpm|yarn|bun> install inidot

Quick Start

import Inidot from "inidot";
// or
import {
  toDotNotation,
  getProperty,
  setProperty,
  deleteProperty,
  hasProperty,
  paths
} from "inidot";

const myObj = {
  name: "Jo,hn",
  age: 21,
  address: { city: "New York", zip: 65221 },
  hobbies: ["Reading", "Traveling", { test: true }],
};

// Get a property by path
Inidot.getProperty(myObj, "address.city");
// "New York"

// Get a missing property → undefined
Inidot.getProperty(myObj, "address.country");
// undefined

// ...or fall back to a default value
Inidot.getProperty(myObj, "address.country", "Morocco");
// "Morocco"

// Set a property (intermediate objects are created automatically)
Inidot.setProperty(myObj, "address.street", "25 Madison Ave");
// myObj.address.street === "25 Madison Ave"

// Check existence
Inidot.hasProperty(myObj, "hobbies.1");
// true

// Delete a property
Inidot.deleteProperty(myObj, "address.zip");
// "zip" is gone

// Flatten an object to dot notation
Inidot.toDotNotation({ a: { b: { c: 1 } } });
// { "a.b.c": 1 }

// Enumerate every leaf path
Inidot.paths(myObj);
// ["name", "age", "address.city", "hobbies"]

Path syntax

A selector is a dot-separated list of keys. Everything below is resolved by the same parser used by getProperty, setProperty, deleteProperty and hasProperty.

| Selector | Parts | Resolves to | | ----------------------- | -------------------------------- | -------------------------------------- | | obj.value | ['obj', 'value'] | { obj: { value } } | | obj.ary.0.value | ['obj', 'ary', '0', 'value'] | { obj: { ary: [{ value }] } } | | obj.ary.*.value | ['obj', 'ary', '*', 'value'] | every value in ary (broadcast) | | obj.ary.0.va\\.lue | ['obj', 'ary', '0', 'va.lue'] | keys containing literal dots | | obj..value | ['obj', 'value'] | empty segments are ignored | | "" | [] | invalid — every operation no-ops |

  • * wildcard skips one array level: get maps across it, set writes to every element, delete removes the key from every element.
  • Numeric segments on arrays select an index (ary.0) — delete splices the element out.
  • Escaped dots \. match a literal . inside a key.

API Reference

toDotNotation(object, prefix?)

Recursively converts an object into a flat object whose keys are dot paths.

toDotNotation({ a: { b: { c: 1 } }, list: [{ x: 2 }, { x: 3 }] });
// { "a.b.c": 1, "list.0.x": 2, "list.1.x": 3 }

toDotNotation({ a: 1 }, "root");
// { "root.a": 1 }

Behavior:

  • Objects and non-empty arrays are recursed into (arrays yield numeric segments).
  • Empty arrays pass through as-is ({ a: [] } → { a: [] }) — remove the inner && (!Array.isArray(value) || value.length) check in src/index.ts to exclude them.
  • Empty objects produce no output at all ({ a: {} } → {}).
  • null and other falsy values are kept as leaves.

getProperty(object, path, value?)

Returns the value at path, or value when the path does not resolve.

getProperty(myObj, "address.city"); // "New York"
getProperty(myObj, "address.country", "Morocco"); // "Morocco"
getProperty(myObj, "hobbies.*.test"); // [undefined, undefined, true]

Semantics worth knowing:

  • The default only applies when the result is undefined — a key holding null returns null, not the default.
  • A key explicitly set to undefined does fall back to the default.
  • * over an array of primitives returns the array as-is; over objects it returns the extracted values.
  • Missing intermediate nodes return undefined instead of throwing.

setProperty(object, path, value)

Sets value at path, auto-creating any missing intermediate objects.

const o: any = {};
setProperty(o, "a.b.c", 1); // { a: { b: { c: 1 } } }

const users: any = { list: [{ score: 1 }, { score: 2 }] };
setProperty(users, "list.*.score", 0); // broadcast to every element
// { list: [{ score: 0 }, { score: 0 }] }
  • Nested keys inside arrays create object elements (a.0.b works on []).
  • Escaped-dot keys are stored literally (a\.b → the key a.b).
  • Empty paths and paths containing disallowed keys no-op (returns undefined — see Safety).

deleteProperty(object, path)

Removes the value at path. path that does not resolve is a no-op.

const o: any = { a: { b: 1, c: 2 } };
deleteProperty(o, "a.b"); // { a: { c: 2 } }

const arr: any = { a: [1, 2, 3] };
deleteProperty(arr, "a.1"); // splice → { a: [1, 3] }
  • Numeric segments splice array elements out.
  • * broadcasts the delete across arrays of objects (a.*.b removes b from every element); over arrays of primitives it is a no-op.
  • Non-object roots never throw.

hasProperty(object, path)

true when the path resolves to anything other than undefined.

hasProperty({ a: null }, "a"); // true  (null is a value)
hasProperty({ a: undefined }, "a"); // false
hasProperty({ a: 0 }, "a"); // true  (falsy-but-present)

paths(object)

Returns every leaf path as dot-notation strings.

paths({ a: { b: { c: 1 }, d: 2 } });
// ["a.b.c", "a.d"]

paths({ a: [1, 2] });
// ["a"]  ← arrays are terminal leaves, not recursed into

paths({ a: 1, b: undefined });
// ["a"]  ← undefined values are skipped

Inidot (default export)

The default export is a class exposing every function as a static — useful when you prefer a namespace over named imports.

import Inidot from "inidot";

Inidot.getProperty(obj, "a.b");
Inidot.setProperty(obj, "a.b", 1);
Inidot.deleteProperty(obj, "a.b");
Inidot.hasProperty(obj, "a.b");
Inidot.toDotNotation(obj);

Safety

Paths are parsed and filtered before any lookup, so you cannot reach or pollute the prototype through a selector. The keys __proto__, prototype and constructor are rejected anywhere in a path — their target resolves to undefined and writes/deletes are silently ignored.

getProperty({}, "__proto__.polluted", 1); // undefined (not the default!)
setProperty({}, "constructor.prototype.polluted", 1); // no-op
({} as any).polluted; // undefined

How it works

All property helpers share one parser in src/index.ts:

  1. Paths without escaped dots take a fast path (split("."), no regex); the escape-aware /(?<!\\)\./ split only runs when the path actually contains a \.
  2. Segments are filtered for the disallowed prototype keys.
  3. Parsed paths are memoized (capped at 10k entries), so repeated paths cost a single map lookup.
  4. The path is walked one key at a time; a * segment broadcasts to every element of the current array, and numeric segments index into arrays.

Benchmarks

Measured with Node v25.9.0 on an Apple Silicon Mac (darwin, arm64), using pnpm benchmark — inidot's src/index.ts compared against a naive implementation, lodash, dot-prop, dlv, dset, flat, get-value and object-path.

| Operation | inidot (ops/sec) | Competitors (ops/sec) | |---|---|---| | getProperty — wide leaf (1000 keys) | 142M | get-value 104M · dlv 55M · naive 44M · lodash.get 69M · object-path 20M · dot-prop 17M | | getProperty — deep (25 levels) | 10.0M | dlv 9.1M · lodash.get 8.3M · get-value 5.4M · object-path 0.95M · dot-prop 0.74M | | getProperty — mixed (arrays) | 42M | dlv 31M · naive 30M · lodash.get 20M · get-value 18M · object-path 7.1M · dot-prop 4.8M | | getProperty — wildcard users.*.name | 32M | naive 3.6M | | setProperty — 25-level chain from {} | 2.4M | naive 2.4M · dset 1.4M · lodash.set 1.3M · object-path 0.83M · dot-prop 0.60M | | setProperty — users.5.name | 13M | naive 11M · dset 6.1M · object-path 5.5M · lodash.set 4.5M · dot-prop 2.6M | | deleteProperty — deep (25 levels) | 396k | naive 394k · lodash.unset 386k · dot-prop 262k · object-path 171k | | deleteProperty — wildcard users.*.age | 703k | naive 631k | | hasProperty — deep (25 levels) | 9.7M | naive 9.2M · lodash.has 5.4M · object-path 1.8M · dot-prop 0.59M | | toDotNotation — deep (25 levels) | 1.6M | naive 1.5M · flat 1.2M | | toDotNotation — wide (1000 keys) | 14.0k | naive 14.0k · flat 10.8k — was 38 ops/sec before the optimization | | toDotNotation — mixed (arrays) | 436k | flat 182k · naive 168k | | paths — deep (25 levels) | 2.1M | naive 1.6M | | paths — mixed | 44M | naive 50M (parity — both under 25 ns/op) |

[!NOTE] Numbers are best-effort microbenchmarks — treat ratios as indicative, not gospel. inidot is the fastest library in every category, tying or beating the naive reference implementation across the board. Two optimizations drive the wins: path parsing is memoized (a path parses once, then costs a map lookup — the same technique lodash uses), and toDotNotation flattens in linear time — the earlier spread-based version was quadratic on wide, leaf-heavy objects (~340× slower).

Caveats

Documented edge cases of the current implementation:

  • setProperty over a primitive leaf throws. Writing through an existing primitive ({ a: 1 } + path a.b.c) walks onto a number/string and a strict-mode TypeError is thrown (Cannot create property 'b' on number '1'). Assigning a leaf (a.b) is always fine — only descending through a primitive is not.
  • getProperty defaults do not cover null. getProperty(o, "a", d) returns d only when the value is undefined; a null value is returned as-is.
  • paths treats arrays as leaves. It walks plain objects only, so an array yields a single path (a), never a.0, a.1, …
  • hasProperty and explicit undefined. A key set to undefined reports false — check "key" in obj if you need presence, not value.
  • Inherited properties are reachable. getProperty({}, "toString") returns the inherited function; the safety filter only blocks __proto__ / prototype / constructor.

Development

pnpm test            # run the test suite once (node:test via tsx)
pnpm test:watch      # re-run tests on every change
pnpm benchmark       # print the benchmark table (~1 min)
pnpm build           # compile src → dist

test/index.ts covers every function, the wildcard/escape/prototype edge cases above, and the default Inidot class. Altogether 67 tests.

Sponsorship

[!NOTE] Enjoy using Inidot? Consider sponsoring us via GitHub Sponsors or PayPal. Your support helps us maintain and improve our services. Thank you! 🫰

License

MIT