This module is ESM 🔆. Please read this.
📖 Full documentation & guides:onury.io/notation
A utility for reading, modifying, and filtering the contents of JavaScript objects and arrays — using object/bracket notation strings or glob patterns.
Notation.create({x: 1}).set('some.prop',true).filter(['*.prop']).value// { some: { prop: true }}Important
This library is intended for data objects with enumerable properties. It does not preserve an object's prototype chain, and does not support objects with circular references.
- Usage
- Notation
- Glob Notation
- Filtering Data with Glob patterns
- Object and Bracket Notation Syntax
- Globs and Data Integrity
- Source Object Mutation
- Documentation
- Quality
Install via NPM:
npm i notationimport{Notation}from'notation';Notation is a class for modifying or inspecting the contents (property keys and values) of a data object or array.
When reading or inspecting an enumerable property value such as obj.very.deep.prop; with pure JS, you would have to do several checks:
if(obj&&obj.hasOwnProperty('very')&&obj.very.hasOwnProperty('deep')&&obj.very.deep.hasOwnProperty('prop')){returnobj.very.deep.prop===undefined ? defaultValue : obj.very.deep.prop;}With Notation, you can do this:
constnotate=Notation.create;returnnotate(obj).get('very.deep.prop',defaultValue);You can also inspect & get the value:
console.log(notate(obj).inspectGet('very.deep.prop'));// {// notation: 'very.deep.prop',// has: true,// value: 'some value',// type: 'string',// level: 3,// lastNote: 'prop'// }To modify or build a data object:
constnotate=Notation.create;constobj={car: {brand: "Dodge",model: "Charger"},dog: {breed: "Akita"}};notate(obj)// initialize. equivalent to `new Notation(obj)`.set('car.color','red')// { car: { brand: "Dodge", model: "Charger", color: "red" }, dog: { breed: "Akita" } }.remove('car.model')// { car: { brand: "Dodge", color: "red" }, dog: { breed: "Akita" } }.filter(['*','!car'])// { dog: { breed: "Akita" }} // equivalent to .filter(['dog']).flatten()// { "dog.breed": "Akita" }.expand()// { dog: { breed: "Akita" }}.merge({'dog.color': 'white'})// { dog: { breed: "Akita", color: "white" }}.copyFrom(other,'boat.name')// { dog: { breed: "Akita", color: "white" }, boat: { name: "Mojo" } }.rename('boat.name','dog.name')// { dog: { breed: "Akita", color: "white", name: "Mojo" }}.value;// result object ^See the documentation for more...
With a glob-notation, you can use wildcard stars * and bang ! prefix. A wildcard star will include all the properties at that level and a bang prefix negates that notation for exclusion.
- Only
Notation#filter()method accepts glob notations. Regular notations (without any wildcard*or!prefix) should be used with all other members of theNotationclass. - For raw Glob operations, you can use the
NotationGlobclass.
Removes duplicates, redundant items and logically sorts the array:
import{NotationGlob}from'notation';constglobs=['*','!id','name','car.model','!car.*','id','name','age'];console.log(NotationGlob.normalize(globs));// ——» ['*', '!car.*', '!id', 'car.model']In the normalized result ['*', '!car.*', '!id', 'car.model']:
idis removed and!id(negated version) is kept. (In normalization, negated always wins over the positive, if both are same).- Duplicate glob,
nameis removed. The remainingnameis also removed bec.*renders it redundant; which covers all possible notations. - (In non-restrictive mode)
car.modelis kept (although*matches it) bec. it's explicitly defined while we have a negated glob that also matches it:!car.*.
console.log(NotationGlob.normalize(globs,{restrictive: true}));// ——» ['*', '!car.*', '!id']- In restrictive mode, negated removes every match.
Note
Notation#filter() and NotationGlob.union() methods automatically pre-normalize the given glob list(s).
Unites two glob arrays optimistically and sorts the result array logically:
constglobsA=['*','!car.model','car.brand','!*.age'];constglobsB=['car.model','user.age','user.name'];constunion=NotationGlob.union(globsA,globsB);console.log(union);// ——» ['*', '!*.age', 'user.age']In the united result ['*', '!*.age', 'user.age']:
- (negated)
!car.modelofglobsAis removed becauseglobsBhas the exact positive version of it. (In union, positive wins over the negated, if both are same.) - But then,
car.modelis redundant and removed bec. we have*wildcard, which covers all possible non-negated notations. - Same applies to other redundant globs except
user.agebec. we have a!*.ageinglobsA, which matchesuser.age. So both are kept in the final array.
When filtering a data object with a globs array; properties that are explicitly defined with globs or implied with wildcards, will be included. Any matching negated-pattern will be excluded. The resulting object is created from scratch without mutating the original.
constdata={car: {brand: 'Ford',model: 'Mustang',age: 52},user: {name: 'John',age: 40}};constglobs=['*','!*.age','user.age'];constfiltered=Notation.create(data).filter(globs).value;console.log(filtered);// ——»// {// car: {// brand: 'Ford',// model: 'Mustang'// },// user: {// name: 'John',// age: 40// }// }In non-restrictive mode; even though we have the !*.age negated glob; user.age is still included in the result because it's explicitly defined.
But you can also do restrictive filtering. Let's take the same example:
constglobs=['*','!*.age','user.age'];constfiltered=Notation.create(data).filter(globs,{restrictive: true}).value;console.log(filtered);// ——»// {// car: {// brand: 'Ford',// model: 'Mustang'// },// user: {// name: 'John'// }// }Note that in restrictive mode, user.age is removed this time; due to !*.age pattern.
Each note (level) of a notation is validated against EcmaScript variable syntax, array index notation and object bracket notation.
x[y],x.1,x.y-z,x.@are incorrect and will never match.x["y"],x['1'],x["y-z"],x['@']are correct object bracket notations.
[0].xindicatesxproperty of the first item of the root array.x[1]indicates second item ofxproperty of the root object.
*is valid wildcard for glob notation. Indicates all properties of an object.[*]is valid wildcard for glob notation. Indicates all items of an array.x[*]is valid wildcard for glob notation. Indicates all items ofxproperty which should be an array.x['*']just indicates a property/key (star), not a wildcard. Valid regular notation.x.*is valid wildcard for glob notation.x,x.*andx.*.*(and so on) are all equivalent globs. All normalize tox.- Negated versions are NOT equivalent.
!xindicates removal ofx.!x.*only indicates removal of all first-level properties ofxbut not itself (empty object).!x.*.*only indicates removal of all second-level properties ofx; but not itself and its first-level properties (x.*).- Same rule applies for bracket notation or mixed notations.
[0]=[0][*]but![0]≠![0][*]x=x[*]but!x≠!x[*][*]=[*].*but![*]≠![*].*
Below, we filter to;
- keep all properties of the source object,
- remove the second item of
colorsproperty (which is an array), - and empty
my-colorsproperty (which is an object).
constsource={name: 'Jack',colors: ['blue','green','red'],'my-colors': {'1': 'yellow'}// non-standard name "my-colors"};constglobs=['*','!colors[1]','!["my-colors"].*'];console.log(Notation.create(source).filter(globs).value);// —» // {// name: 'Jack',// colors: ['blue', 'red'],// 'my-colors': {}// }In the example above, colors item at index 1 is emptied.
In a glob list, you cannot have both object and array notations for root level. The root level implies the source type which is either an object or array; never both.
For example, ['[*]', '!x.y'] will throw because when you filter a source array with this glob list; !x.y will never match since the root x indicates an object property (e.g. source.x).
Each glob you use should conform with the given source object.
For example:
constobj={x: {y: 1}};constglobs=['*','!x.*'];console.log(Notation.create(obj).filter(globs).value);// ——» { x: {}}Here, we used !x.* negated glob to remove all the properties of x but not itself. So the result object has an x property with an empty object as its value. All good.
But in the source object; if the actual value of x is not an object, using the same glob list would throw:
constobj={x: 1};// x is numberconstglobs=['*','!x.*'];console.log(Notation.create(obj).filter(globs).value);// ——» ERRORThis kind of type mismatch is critical so it will throw. The value 1 is a Number not an object, so it cannot be emptied with !x.*. (But we could have removed it instead, with glob !x.)
The source object or array will be mutated by default (except the #filter() method). To prevent mutation; you can call #clone() method before calling any method that modifies the object. The source object will be cloned deeply.
constnotate=Notation.create;constmutated=notate(source1).set('newProp',true).value;console.log(source1.newProp);// ——» trueconstcloned=notate(source2).clone().set('newProp',true).value;console.log('newProp'insource2);// ——» falseconsole.log(cloned.newProp);// ——» trueWarning
Notation expects a data object (or array) with enumerable properties. In addition to plain objects and arrays; supported cloneable property/value types are primitives (such as String, Number, Boolean, Symbol, null and undefined) and built-in types (such as Date and RegExp).
Enumerable properties with types other than these (such as methods, special objects, custom class instances, etc) will be copied by reference. Non-enumerable properties will not be cloned.
If you still need full clone support, you can use a library like lodash. e.g. Notation.create(_.cloneDeep(source))
Read the full documentation — guides, concepts and the API reference. For what changed in v3, see the change log.
Read the CHANGELOG.
- 100% test coverage (statements, branches, functions, lines) — enforced via
Vitest thresholds. Run
npm run cover. - ~84% mutation score via StrykerJS. Run
npm run mutation.
Mutation testing goes beyond coverage: it
makes hundreds of small edits ("mutants") to the source — flipping > to >=,
&& to ||, returning undefined, etc. — and checks that a test fails for
each. It catches tests that execute code without actually asserting its
behavior (the trap where function getPositive(x){ return x } reaches 100%
coverage but verifies nothing). Most of the surviving mutants here are
equivalent mutants
in the glob normalization/cover/union logic — redundant-but-harmless branches
that produce identical results — plus environment-defensive guards. These can't
be killed by definition, so the realistic, healthy target is a high score, not
100%.
- accesscontrol — Role and attribute based access control (RBAC + ABAC) with conditions, enforced ownership, custom actions and mandatory gates.
- configuard — Turn flat config rows from a database table into a nested, typed configuration object — with
${...}templating and accessor-based (ABAC) filtering.
© 2026, Onur Yıldırım. MIT License.