A mapper turns a vendor response into your own shape, and it is forty lines of response.customer && response.customer.address && response.customer.address.city. Optional chaining shortens each line
and leaves forty of them. When the vendor adds a field, somebody writes line forty-one, and the
mapping nobody can see as a whole grows again.
What you get
You will end up reading nested values by a path held as data, so a mapper becomes a table. This is for you if your field mapping is a wall of guarded property access.
Short answer
Use a path reader that takes the object and a dotted path, and returns undefined when any step is missing. Optional chaining does the same job when the path is known while you are writing the code. A path reader is for when the path is data: a mapping table, a configuration file, or a column a user picked.
You will need
Node 22 or later, and a nested response you map onto your own shape. The example uses getpath from struct, which takes the object first and the path second.
Approaches compared
| Approach | When it fits | What it costs you | When to pick something else |
|---|---|---|---|
| just-safe-get | You want one small function with no wider dependency | A package per operation, so a project doing several of these collects several | You already depend on a library that includes this |
| lodash.get | Any project, and the most widely recognized of the four | A dependency whose main package is large, though this entry point is not | Nothing in the project needs lodash otherwise |
| Optional chaining | The path is known when you write the line | The path cannot come from data, so every field is a line of code | The path is configuration, or chosen at runtime |
| struct getpath | You already use struct for merging and transforming the same data | A smaller ecosystem, and its argument order differs from lodash | lodash is already in the project |
Voxgig maintains struct. It is one of four options here, not the recommendation.
The real split is between syntax and data. Optional chaining is syntax: it compiles to the same guarded access you would write, and it costs nothing at runtime. The three functions take the path as a value, which is what a mapping table needs, and they pay a small cost per read to parse it.
Turn the mapper into a table
Once the path is data, the mapping is a table rather than a function body.
export function field(response, path, fallback = null) {
const value = getpath(response, path)
return value === undefined ? fallback : value
}
/** Read several paths at once, which is the usual shape of a response mapper. */
export function fields(response, paths) {
return Object.fromEntries(Object.entries(paths).map(([name, path]) => [name, field(response, path)]))
}
The fields helper is the reason to bother. A mapping from your names to the vendor’s is now an object literal a
reviewer reads in one go. Adding a field is a line of data rather than a line of code. That table can also come from a config file, which is what makes one mapper serve two vendors
without a branch in the code.
Substituting a fallback for undefined is a decision worth making once, centrally. A missing value and a value that is genuinely absent read the same way to the code downstream. Choosing null rather than undefined means the result serializes to JSON with the key present.
See the three readers agree
The functions differ in ergonomics rather than in results, which is the useful thing to establish before choosing between them.
node compare.mjs
customer.address.city optional chaining: "Dublin" lodash.get: "Dublin" struct getpath: "Dublin"
customer.address.postcode optional chaining: undefined lodash.get: undefined struct getpath: undefined
lines.0.amount optional chaining: 2000 lodash.get: 2000 struct getpath: 2000
missing.deep.path optional chaining: undefined lodash.get: undefined struct getpath: undefined
All three return the same value for every path here. That covers the array index and the path that stops at its very first segment. The difference is in the source. The optional chaining column needs one branch per path written out, and the other two take the path as an argument.
That equivalence is worth checking rather than assuming, because the libraries differ at the edges.
lodash.get accepts bracket notation such as lines[0].amount as well
as dots, and it substitutes a default you pass as a third argument. A reader written against one
convention and swapped for another returns undefined for the paths that used the old syntax, and
undefined is what a missing field looks like too.
Check it worked
The cases that matter are the missing ones, because that is where a hand-written chain throws. The fourth test maps three paths at once, which is the shape the mapper actually takes in production.
node --test read.test.mjs
1..4
# tests 4
# suites 0
# pass 4
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 129.541597
When it goes wrong
The argument order is the first thing to get wrong. getpath takes the object first, and lodash
takes the object first, and several other libraries take the path first. Calling one with the other
order returns undefined rather than throwing, so the mapper produces nulls everywhere and the tests
that check a missing field still pass.
The second failure is a key containing a dot. A path is split on dots, so a response whose keys are domain names or version numbers cannot be addressed with a dotted string. Pass the path as an array of segments, which every one of these functions accepts, and the ambiguity disappears. That form is also faster, because nothing has to be split before the read.
The third is a path reader used where a schema belongs. Reading twelve paths tells you nothing about whether the response was the shape you expected. A mapper full of nulls is what a changed API looks like from the inside, and it is indistinguishable from a customer whose record is genuinely empty.
When not to do this
Do not replace optional chaining with a function call where the path is a literal. The syntax is clearer at the point of use, has no dependency, and the compiler checks the property names against your types.
Do not reach for struct to get one function. The package earns its place when you also merge, transform or walk the same data, and a single path read is better served by the syntax or by a one-function package.
Do not use a fallback to hide a required field. A mapper returning null for a value your code needs moves the failure from the boundary to wherever that null is finally used. Keep the fallback for values that really are optional, and let a missing required field raise where the response is read.
Do not build paths from user input without restricting them. A path is a read into arbitrary structure, and one taken from a query parameter lets a caller address parts of an object you never meant to expose.
Related how-tos
Last verified
Verified 2026-09-06 against Node 22.22.2, @voxgig/struct 0.3.5 and lodash.get 4.4.2. Both output blocks are what the preceding command printed.