inidot
v2.0.0
Published
Get, set, or delete a property from a nested object using a dot path
Maintainers
Readme
Inidot :pencil:
Get, set, or delete a property from a nested object using a dot path :fire:
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,prototypepaths 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 inidotQuick 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:getmaps across it,setwrites to every element,deleteremoves the key from every element.- Numeric segments on arrays select an index (
ary.0) —deletesplices 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 insrc/index.tsto exclude them. - Empty objects produce no output at all (
{ a: {} }→{}). nulland 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 holdingnullreturnsnull, not the default. - A key explicitly set to
undefineddoes 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
undefinedinstead 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.bworks on[]). - Escaped-dot keys are stored literally (
a\.b→ the keya.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.*.bremovesbfrom 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 skippedInidot (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; // undefinedHow it works
All property helpers share one parser in src/index.ts:
- Paths without escaped dots take a fast path (
split("."), no regex); the escape-aware/(?<!\\)\./split only runs when the path actually contains a\. - Segments are filtered for the disallowed prototype keys.
- Parsed paths are memoized (capped at 10k entries), so repeated paths cost a single map lookup.
- 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
toDotNotationflattens 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:
setPropertyover a primitive leaf throws. Writing through an existing primitive ({ a: 1 }+ patha.b.c) walks onto a number/string and a strict-modeTypeErroris thrown (Cannot create property 'b' on number '1'). Assigning a leaf (a.b) is always fine — only descending through a primitive is not.getPropertydefaults do not covernull.getProperty(o, "a", d)returnsdonly when the value isundefined; anullvalue is returned as-is.pathstreats arrays as leaves. It walks plain objects only, so an array yields a single path (a), nevera.0,a.1, …hasPropertyand explicitundefined. A key set toundefinedreportsfalse— check"key" in objif 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 → disttest/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! 🫰

