A row of opened nesting dolls with a small key beside the innermost one, watched by a calm gray-blue cat

get-value: Reaching Into Nested Objects Without Faceplanting

The Gray Cat
The Gray Cat
0 views

Somewhere in your node_modules right now, several copies of a tiny module are quietly pulling values out of deeply nested config objects. You probably did not install it on purpose. It rode in through a build tool, a template engine, or a linter, and it does one small job so reliably that nobody ever notices it. That module is get-value, and it has the kind of download numbers most marquee frameworks would envy: around 15.7 million a week, with zero runtime dependencies and a footprint you can measure in a couple of kilobytes.

The pitch is simple. You have an object, you have a path like 'a.b.c', and you want the value at the end of that path without writing a chain of && checks or wrapping everything in a try/catch. get-value walks the path for you and hands back undefined (or a default you choose) when any link in the chain is missing. It never throws on a missing intermediate key. And it has one trick that almost none of its competitors get right: it works even when the keys themselves contain dots.

Why This Still Earns Its Keep

Modern JavaScript already has optional chaining, so obj?.a?.b?.c covers a lot of ground that utilities like this used to own. That is true, and it is the first thing worth being honest about. If your path is known at author time, the language feature is the better answer. The reason get-value keeps shipping tens of millions of times a week is that real applications are full of paths that are not known at author time.

Think about a configuration system where users supply the path as a string. Or a table component that reads row[column.accessor] where accessor is 'address.city'. Or a templating engine resolving {{ user.profile.avatar }} against a data object. In every one of those cases the path is a runtime value, and optional chaining simply cannot express it. You need a function that takes a string and walks the object. That is exactly the gap get-value fills, and it fills it with a few features that hand-rolled loops tend to miss:

  • Dotted keys. It correctly resolves 'a.b.c' against { 'a.b': { c: 'd' } }, where the literal key 'a.b' contains a dot. This is genuinely hard, and most one-liners get it wrong.
  • Default values. Pass a default and you get a clean fallback instead of undefined, without an extra ?? at every call site.
  • Array paths. Hand it ['a', 'b'] instead of a string to sidestep splitting ambiguity entirely.
  • Custom separators and validation. Swap the delimiter, plug in your own split/join logic, or validate each resolved segment with an isValid guard.
  • Zero dependencies. Nothing else comes along for the ride, which is a big part of why so many tools are comfortable depending on it.

Getting It Into Your Project

Installation is the usual one-liner, and there is nothing else to configure.

npm install get-value

Or with yarn:

yarn add get-value

Version 4 was rewritten in TypeScript, so the type definitions ship in the box. No @types/get-value package to chase down.

Walking a Path Without Tripping

The core call takes the object first, the path second, and an optional options object third. Here it is doing the thing it was born to do.

import getValue from 'get-value';

const data = {
  user: {
    profile: {
      name: 'Ada',
      address: { city: 'London' },
    },
  },
};

getValue(data, 'user.profile.name');
//=> 'Ada'

getValue(data, 'user.profile.address.city');
//=> 'London'

The payoff shows up when the path does not fully exist. Where a raw data.user.account.balance would throw a TypeError the moment account turned out to be undefined, get-value simply returns undefined and moves on.

getValue(data, 'user.account.balance');
//=> undefined  (no throw, no crash)

Numeric segments index into arrays, so you can dig through lists the same way you dig through objects. The path stays a flat string regardless of how the structure is shaped underneath.

const order = {
  items: [
    { sku: 'A1', qty: 2 },
    { sku: 'B7', qty: 1 },
  ],
};

getValue(order, 'items.1.sku');
//=> 'B7'

A Default Instead of a Shrug

Returning undefined is safe, but often what you actually want is a sensible fallback. The default option lets you bake that in so the call site reads cleanly.

const settings = { theme: { mode: 'dark' } };

const accent = getValue(settings, 'theme.accent', {
  default: 'blue',
});
//=> 'blue'

const mode = getValue(settings, 'theme.mode', {
  default: 'light',
});
//=> 'dark'  (the real value wins; the default only fills gaps)

This is the moment get-value starts to feel ergonomic rather than merely safe. Instead of writing getValue(obj, path) ?? fallback at every call, the fallback travels with the lookup as part of its definition.

The Trick Nobody Else Pulls Off

Here is the feature that explains why get-value outlived a lot of similar utilities. Real-world data sometimes has keys with dots in them: think of a flattened config, a file path used as a key, or a versioned field like '1.0.0'. Naively splitting on . would shatter those keys into pieces and the lookup would fail. get-value is smart enough to check whether a multi-segment key exists as a single literal key before it keeps descending.

const flat = {
  'a.b': { c: 'd' },
};

getValue(flat, 'a.b.c');
//=> 'd'  (it recognized 'a.b' as one real key, then read 'c')

If you would rather avoid the ambiguity altogether, hand the path in as an array of segments. Each element is treated as one literal key, so dots inside a segment are never interpreted as separators.

const odd = {
  'release.notes': { '1.0': 'Initial cut' },
};

getValue(odd, ['release.notes', '1.0']);
//=> 'Initial cut'

That array form is the most robust way to drive get-value from code, because it removes every guess about where one key ends and the next begins.

Bending the Rules: Separators and Validation

When the dot does not suit your data, you can change the delimiter. The separator option accepts a string or a regular expression, and there are companion split, join, and joinChar hooks for the cases where you need full control over how paths are torn apart and stitched back together.

const data = {
  a: { b: { c: 'found it' } },
};

getValue(data, 'a/b/c', { separator: '/' });
//=> 'found it'

For situations where you care not just whether a value exists but whether it is acceptable, the isValid option lets you screen each resolved segment. It receives the key and the object it was read from, so you can reject inherited properties, skip over functions, or enforce your own rules about what counts as a real hit.

const config = {
  db: { host: 'localhost', port: 5432 },
};

const value = getValue(config, 'db.toString', {
  isValid: (key, obj) => Object.hasOwn(obj, key),
});
//=> undefined  (toString is inherited, so isValid rejects it)

Version 4.1 extended the reach of the library to Map-like objects as well, so structures that expose a get-style interface rather than plain property access can participate in the same path-walking logic. Combined with the TypeScript rewrite in version 4.0 and improved array handling, the modern releases keep the original tiny API while quietly broadening what counts as a traversable object.

How It Stacks Up

It helps to know where get-value sits among the other tiny path-readers, because the right choice depends on what you value.

  • dlv is the minimalist champion at roughly 130 bytes and tens of millions of weekly downloads. It is dlv(obj, 'a.b.c') and almost nothing else. No options object, no dotted-key handling. Reach for it when every byte counts and your keys are well-behaved.
  • lodash.get is the familiar classic with a default-value argument and a comfortable API. It carries the lodash lineage and does not resolve dotted keys the way get-value does, and the standalone package is effectively in maintenance mode.
  • just-safe-get comes from the dependency-free just family and offers a clean get(obj, 'a.b.c'). It is a fine pick if you are already living in that ecosystem, though its community is smaller.

The case for get-value is correctness plus configurability in a package that still costs almost nothing. If your data is tidy and static, optional chaining or dlv will serve you. The moment paths become dynamic, keys get weird, or you want defaults and validation built into the lookup, this is the utility that keeps doing the right thing.

The Quiet Workhorse

There is something fitting about a library this small being one of the most-installed packages in the registry. It does not ask for attention. It has two open issues, a couple hundred stars, and a maintainer who has been refining the same focused idea for years. What it offers is a guarantee: hand it an object and a path, and it will give you back a value or a sensible default without ever blowing up in your face, even when the keys have dots in them.

If you have been hand-writing nested-access helpers, or scattering ?. chains across code that really needs a runtime path, get-value is worth the install. It is the kind of dependency you add once, never think about again, and quietly rely on for years.