Skip to content

safeChain for safe chaining ​

This post was organized on December 7, 2019.

Purpose of the article ​

Various techniques are used to safely use objects and arrays. The methods include assigning a default value when undefined or using a conditional statement. And recently, optional chaining syntax is also used to safely chain.

What I tried this time was to create a function for safe chaining. I made it to prevent readability and complexity from increasing with basic value assignment or conditional statements, or in environments that do not support optional chaining-like syntax.

Order ​

First, I organized the order of how to use the function and the implementation code. And I wrote a case that can be applied in practice.

  • How to use
  • Code
  • Use case

How to use ​

In normal situations, it extracts normal values.

js
const obj = {a: {b: {c: 'c'}}}
const dot = safeChain(obj, (obj) => obj.a.b.c)
const destruct = safeChain(obj, ({a: {b: {c}}}) => c)

console.log(dot) // c
console.log(destruct) // c

In abnormal situations, it returns undefined.

js
const obj = {a: {b: {c: 'c'}}}
const dot = safeChain(obj, (obj) => obj.c.b.a)
const destruct = safeChain(obj, ({c: {b: {a}}}) => a)

console.log(dot) // undefined
console.log(destruct) // undefined

If you use the basic syntax, this error will occur.

js
const obj = {a: {b: {c: 'c'}}}
const {c: {b: {a}}} = obj
// Uncaught TypeError: Cannot read property 'b' of undefined

Code ​

safeChain main function ​
  • Wraps the state received as an argument in a proxy.
  • Executes the mapper function received as an argument.
  • If the result of the mapper function is wrapped (isPack), it is unpacked (unpack).
    • Otherwise, it returns immediately.
  • Finally, cancel all proxies and return the result.
js
const safeChain = (state, mapper) => {
  const revokes = []
  const proxy = toProxy(state, revokes)
  const mappedResult = mapper(proxy)
  const result = isPack(mappedResult) ? unpack(mappedResult) : mappedResult

  revokes.forEach(fn => fn())
  return result
}
Helper code ​
js
const symbol = Symbol('safe')

const isNullish = value => value === undefined || value === null
const isPack = pack => typeof pack === 'object' && symbol in pack

const pack = value => ({ [symbol]: value })
const unpack = pack => pack[symbol]
Core function ​
  • toProxy wraps (pack) the state and creates a proxy.
  • trap handles the state when it is queried.
    • If it is a key (symbol) defined internally, it returns the value.
    • Otherwise
      • When undefined or null, wrap undefined in a proxy.
      • If it is a normal value, wrap the normal value in a proxy.
js
const toProxy = (state, revokes) => {
  const wrap = pack(state)
  const handler = trap(revokes)
  const {proxy, revoke} = Proxy.revocable(wrap, handler)
  revokes.push(revoke)
  return proxy
}

const trap = (revokes) => {
  return {
    get (target, key) {
      const unpacked = unpack(target)
      return key === symbol ?
        unpacked :
        toProxy(isNullish(unpacked) ? undefined : unpacked[key], revokes)
    }
  }
}

Use case ​

The backend API response can be used after the API response occurs. However, if you query the value in the getter and use it in the vue, angular component, a reference error occurs. In this case, you can prevent errors by using safeChain.

It is a code that assumes the use of the backend API.

js
const backendApi = {
  fetchResponse() {
    this.response = {
      result: { message: 'Success' },
      status: 200,
      statusCode: 'Ok'
    }
  },
  response: null,
  get result () {
    if (this.response && this.response.result) {
      return this.response.result
    }
    return undefined
  }
}

console.log(backendApi.result) // undefined
backendApi.fetchResponse()
console.log(backendApi.result) // { message: 'Success' }

You can remove the conditional statement by using the safeChain function.

js
const backendApi = {
  fetchResponse() {
    this.response = {
      result: { message: 'Success' },
      status: 200,
      statusCode: 'Ok'
    }
  },
  response: null,
  get result () {
    return safeChain(this.response, ({result}) => result)
  }
}

console.log(backendApi.result) // undefined
backendApi.fetchResponse()
console.log(backendApi.result) // { message: 'Success' }

End ​