Based on this answer, I've built my own custom function that subsets an object, to keep only some of its properties. Problem is, the code breaks if I point to a property that doesn't exist.
Essentially, I'm looking to mimic the functionality of the optional chaining operator (?.
) , but in the context of my own custom function.
My current function implementation
The function accepts two inputs:
- The object to subset
- An object that specifies the path of each property we want to keep, as well as a name for the key of this value in the new object.
The function's output is a new object, which is a subset of the original. By design, this is an immutable implementation.
// typescript
const is = (t: any, T: any) => t?.constructor === T;
const getProp = <T>(obj: any, key: string): T => obj[key] ?? null;
const getValue = <T>(pathToVal: string[], origData: T): T => pathToVal.reduce(getProp, origData);
const select = (data: Object, paths: Record<string, string[]>) =>
!is(paths, Object) // if `paths` is not object, fail now
? new Error('path supplied not an object')
: // else do what we want
Object.entries(paths).reduce(
(res: object, [key, path]: [string, string[]]) => Object.assign(res, { [key]: getValue(path, data) }),
{}
);
Here is an example how to call select()
.
const earthData = {
distanceFromSun: 149280000,
continents: {
asia: {
area: 44579000,
population: 4560667108,
countries: { japan: { temperature: 62.5 } },
},
africa: { area: 30370000, population: 1275920972 },
europe: { area: 10180000, population: 746419440 },
america: { area: 42549000, population: 964920000 },
australia: { area: 7690000, population: 25925600 },
antarctica: { area: 14200000, population: 5000 },
},
};
const earthDataSubset = select(earthData, {
distanceFromSun: ['distanceFromSun'],
asiaPop: ['continents', 'asia', 'population'],
americaArea: ['continents', 'america', 'area'],
japanTemp: ['continents', 'asia', 'countries', 'japan', 'temperature'],
});
console.log(earthDataSubset)
// { distanceFromSun: 149280000,
// asiaPop: 4560667108,
// americaArea: 42549000,
// japanTemp: 62.5 }
The problem
Requesting to subset a property that doesn't exist results in an error, instead of simply returning null
for that non-existing path.
const earthDataSubset2 = select(earthData, {
distanceFromSun: ["distanceFromSun"],
asiaPop: ["continents", "asia", "population"],
americaArea: ["continents", "america", "area"],
japanTemp: ["continents", "asia", "countries", "japan", "temperature"],
foo: ["bar", "baz"] // <~~~~ this is the addition that breaks the code
});
console.log(earthDataSubset2)
// Cannot read properties of null (reading 'baz')
But what I expect to get is:
// expected output
console.log(earthDataSubset2)
// { distanceFromSun: 149280000,
// asiaPop: 4560667108,
// americaArea: 42549000,
// japanTemp: 62.5,
// foo: null
}
Also please note that if the non-existing property is at the end of the path, we do get null
as expected.
const earthDataSubset3 = select(earthData, {
distanceFromSun: ["distanceFromSun"],
asiaPop: ["continents", "asia", "population"],
americaArea: ["continents", "america", "area"],
japanTemp: ["continents", "asia", "countries", "japan", "temperature"],
foo: ["continents", "asia", "countries", "japan", "temperature", "baz"] // <~~~~ this returns null as expected
});
console.log(earthDataSubset3)
// { distanceFromSun: 149280000,
// asiaPop: 4560667108,
// americaArea: 42549000,
// japanTemp: 62.5,
// foo: null }
Bottom line, my question is how I could alter the current definition of select()
to account for situations where a non-existing property could be at any position in that specified path, not only at the path's end.
CodePudding user response:
It is your getProp()
that's the issue – it errors when obj
is null
in obj[key]
, since you cannot access properties on null
. This happens when a top level object property does not exist on the object being select
ed. We can use optional chaining to work around this:
const getProp = <T>(obj: any, key: string): T => obj?.[key] || null;
The
?.
operator is like the.
chaining operator, except that instead of causing an error if a reference is nullish (null
orundefined
), the expression short-circuits with a return value ofundefined
.
const is = (t, T) => t.constructor === T;
const getProp = (obj, key) => obj?.[key] || null;
const getValue = (pathToVal, origData) => pathToVal.reduce(getProp, origData);
const select = (data, paths) =>
!is(paths, Object)
? new Error('path supplied not an object')
: // else do what we want
Object.entries(paths).reduce(
(res, [key, path]) => Object.assign(res, { [key]: getValue(path, data) }),
{}
);
const earthData = {
distanceFromSun: 149280000,
continents: {
asia: {
area: 44579000,
population: 4560667108,
countries: { japan: { temperature: 62.5 } },
},
africa: { area: 30370000, population: 1275920972 },
europe: { area: 10180000, population: 746419440 },
america: { area: 42549000, population: 964920000 },
australia: { area: 7690000, population: 25925600 },
antarctica: { area: 14200000, population: 5000 },
},
};
const earthDataSubset2 = select(earthData, {
distanceFromSun: ["distanceFromSun"],
asiaPop: ["continents", "asia", "population"],
americaArea: ["continents", "america", "area"],
japanTemp: ["continents", "asia", "countries", "japan", "temperature"],
foo: ["bar", "baz"] // <~~~~ this is the addition that breaks the code
});
console.log(earthDataSubset2)