The Rodeo del Jefe

Writings and collections of Robert Jefe Lindstaedt

Deep Map Object Values

Given a deep object, for example a frontend feature flag map, you might want to allow all flags to be true in development and have the actual value only when built.

TL;DR

function mapObject (obj, fn) {
  return Object.keys(obj).reduce(
    (res, key) => {
      res[key] = fn(obj[key])
      return res
    },
    {}
  )
}

function deepMap (obj, fn) {
  const deepMapper = val => (typeof val === 'object' ? deepMap(val, fn) : fn(val))

  if (Array.isArray(obj)) {
    return obj.map(deepMapper)
  }

  if (typeof obj === 'object') {
    return mapObject(obj, deepMapper)
  }

  return obj
}

module.exports = deepMap

Prelude: A Feature Flagging Implementation

Imagine a SPA that has those constants and environment variables that will be defined by the built pipeline as process.env globals and uses a feature flagging library like React Flag or any implmenetation similar to Martin Fowlers classic.

const { inDevelopEnvironment } = require('../utils/env')
const { deepMap } = require('../utils/objects/map')

const FLAGS = {
  features: {
    login: {
      passwordForgotten: true
    },
    globalSearch: true,
    settings: {
      general: {
        general: false,
        vouchers: true,
        profile: false,
        payments: false
      },
      security: false
    },
    products: false,
    accounts: function () {
      // something more elaborate here
      return false
    }
  },
  pages: {
    login: {
      passwordForgotten: false
    }
  }
}

function getFlags () {
  if (inDevelopEnvironment() && process.env.FLAGS !== 'true')
  return FLAGS
}

export const flags = getFlags()

Conveneniently we allow us to simply run npm start and the whole app will be displayed. Running FLAGS=true npm start or any other NODE_ENV than development shows what the user will see.

Hence inDevelopEnvironment is defined as

export function inDevelopEnvironment () {
  return process.env.NODE_ENV === 'development'
}

The Solutions

Now, the problem is that JavaScript does not have a any primitive yet, to map through all the keys or values of an object. There are many StackOverflow suggestions, which I list below.

#1: Modern JavaScript

For the above case, the one with the least assumptions about your application logic and inputs, as well as the most readable, is the following, by geoffroy-warin you can find here, to this question.

export function mapObject (obj, fn) {
  return Object.keys(obj).reduce(
    (res, key) => {
      res[key] = fn(obj[key])
      return res
    },
    {}
  )
}

export function deepMap (obj, fn) {
  const deepMapper = val => (typeof val === 'object' ? deepMap(val, fn) : fn(val))

  if (Array.isArray(obj)) {
    return obj.map(deepMapper)
  }

  if (typeof obj === 'object') {
    return mapObject(obj, deepMapper)
  }

  return obj
}

#2: The JSON.stringify way, for the lulz

JSON.stringify is an extremely powerful tool, but admittedly also the seemingly most hacky and littlest expressive one. However, let’s see some for old time’s sake.

We gonna make use of JSON.stringify’s replacer feature. Head over to MDN if you like.

export function deepMap (obj, fn) {
  function replacer (key, value) {
    if (typeof value !== 'object') {
      return fn(value)
    }

    return value
  }
  return JSON.parse(JSON.stringify(obj, replacer))
}

See a working version here.

NOTE: This will not preserve non JSON supported keys and values.

Approved!