2
function createElement (type, { attrs = {}, children = [] }) {
 if (Object.prototype.toString.call(arguments[1]) !== '[object Object]') { 
   throw Error('The options argument must be an object'); 
 }

 return {
  type,
  attrs,
  children
 }
}

I have a function which takes two arguments: a string and an object. In the function declaration, I am unpacking the object's values through destructuring.

When it comes to making sure the second argument is an object, I know I can do this check: Object.prototype.toString.call(arguments[1] !== 'object Object').

But if null or undefined are passed as arguments, this error occurs: Uncaught TypeError: Cannot destructure property 'attrs' of 'undefined' or 'null'.. This is understandable because null and undefined cannot be coerced into objects. What can I do to guard against this?

If an array, number etc is passed as a second argument, no error is thrown because they can be coerced and I can then handle those values in the function body. When dealing with null or undefined, the code in the function is never executed.

// expected behaviour
createElement('div', []); // Uncaught Error: The options argument must be an object
createElement('div', function(){}); // Uncaught Error: The options argument must be an object
createElement('div', false); // Uncaught Error: The options argument must be an object
createElement('div', new Date()); // Uncaught Error: The options argument must be an object
createElement('div', 4); // Uncaught Error: The options argument must be an object

// unwanted behaviour
createElement('div', null); // Uncaught TypeError: Cannot destructure property `attrs` of 'undefined' or 'null'
createElement('div', undefined); // Uncaught TypeError: Cannot destructure property `attrs` of 'undefined' or 'null'

Edited to provide final solution: After reading the comments, it appears the only solution is either to allow the exception to be thrown or to destructure the code in the function body and handle the error. This is the solution I have chosen:

createElement (type, opts) {
  if (arguments[1] !== undefined && Object.prototype.toString.call(opts) !== '[object Object]') { 
    throw Error('The options argument must be an object'); 
  }

  const { attrs = {}, children = [] } = opts || {};

  return {
    type,
    attrs,
    children
  }
}

5 Answers5

3

With default values.

function createElement(type, { attrs, children } = {}) {
  return {
    type,
    attrs,
    children
  }
}

console.log(createElement("foo"));
console.log(createElement("foo", undefined));
mbojko
  • 13,503
  • 1
  • 16
  • 26
  • If you pass `null` or `undefined` as your second argument, you will receive the same error I am receiving. – Carl Mungazi Apr 11 '19 at 18:31
  • 1
    `null` yes, undefined - no. – mbojko Apr 11 '19 at 18:32
  • Yes, you are right. Thanks. That leaves `null` now. – Carl Mungazi Apr 11 '19 at 18:37
  • To guard yourself against a `null`, I think you'll need to build on Code Maniac's response, and do restructuring inside the function body after a check `if (obj === null)`. I don't believe there is a neat trick that can handle it already within the function signature. But maybe StackOverflow will prove me wrong. – mbojko Apr 11 '19 at 18:40
  • @mbojko you can something like [this](https://stackoverflow.com/questions/54931090/capturing-nested-levels-in-javascript-destructuring/54931140#54931140) but that's hacky – Code Maniac Apr 11 '19 at 18:45
2

You could defer the destructuring and take a check in advance.

function createElement (type, object) {
   if (!object || typeof object !== 'object' || Array.isArray(object)) { 
       throw Error('The options argument must be an object'); 
   }
   var { attrs = {}, children = [] } = object;
   return { type, attrs, children };
}
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • Yes, I think I will do the destructuring in the function body. However, I tried your solution and noticed that arrays still pass the check, which I wouldn't want them to. I will probably stick to using `Object.prototype.toString.call(arg1) !== '[object Object]'` as my one-stop check since it would handle everything, including `null` and `undefined`. – Carl Mungazi Apr 11 '19 at 19:09
2

This function probably does what you want (allow a null or undefined second arg to be defaulted):

function createElement (type, obj) {
  const arg1 = arguments[1];
  if (arg1 !== null && arg1 !== undefined &&
      Object.prototype.toString.call(arg1) !== '[object Object]') {
    throw Error('The options argument must be an object'); 
  }
  let { attrs = {}, children = [] } = obj || {}
  return {
    type,
    attrs,
    children
  }
}

This function also fixes your object test to work properly.

[UPDATED]

If you do want null to also throw the non-object Error, you can use this:

function createElement (type, obj) {
  const arg1 = arguments[1];
  if (arg1 !== undefined &&
      Object.prototype.toString.call(arg1) !== '[object Object]') {
    throw Error('The options argument must be an object'); 
  }
  let { attrs = {}, children = [] } = obj || {}
  return {
    type,
    attrs,
    children
  }
}
cybersam
  • 63,203
  • 6
  • 53
  • 76
1

I will suggest to destructure inside function instead of doing it in definiation of function

function createElement (type, obj) {
  let { attrs = {}, children = [] } = obj || {attrs:undefined,children:undefined}
  return {
    type,
    attrs,
    children
  }
}


console.log(createElement('some type',undefined))
Code Maniac
  • 37,143
  • 5
  • 39
  • 60
  • After doing some research last night, I also came to this conclusion. But I was also curious to know if there was another way of handling this. I came across this function whilst researching the way virtual-dom is used by different frameworks. I noticed that none of them destructure function parameters this way. Maybe this is why? – Carl Mungazi Apr 11 '19 at 18:35
  • @CarlMungazi yes you can do hacky things but it's better to keep code clean and clear so it becomes easy to maintain, you can see these [1 destructuing](https://stackoverflow.com/questions/55194118/how-do-i-parse-a-string-to-number-while-destructuring) [2 paseint while destructuring](https://stackoverflow.com/questions/55194118/how-do-i-parse-a-string-to-number-while-destructuring) – Code Maniac Apr 11 '19 at 18:39
  • Thanks. That answer made for interesting reading, especially the hacky answer and this: `[latitude, longitude] = ['17.0009','82.2108'].map(Number);` – Carl Mungazi Apr 11 '19 at 18:53
0
In case when object is undefined use {property1,property2}={}

In case when property is undefined use {property1={},property2={}}

In your case object is undefined so below code will be used:

function createElement (type, { attrs = {}, children = [] }={}) {
 if (Object.prototype.toString.call(arguments[1]) !== '[object Object]') { 
   throw Error('The options argument must be an object'); 
 }

 return {
  type,
  attrs,
  children
 }
}
Raj Kumar
  • 402
  • 4
  • 9