Home > front end >  Building a custom function for creating a new object as a subset of object properties of any depth:
Building a custom function for creating a new object as a subset of object properties of any depth:

Time:08-02

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:

  1. The object to subset
  2. 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 selected. 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 or undefined), the expression short-circuits with a return value of undefined.

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)

  • Related