Charlie
Charlie

Reputation: 830

ES6 destructuring preprocessing

Problem

Function arguments destructuring is an amazing feature in ES6. Assume we want a function named f to accept an Object which has an a key

function f({ a }) {
    return a;
}

We have default values for the case when parameter wasn't provided to a function in order to avoid Type Error

function f({ a } = {}) {
    return a;
}

This will help in the following case

const a = f(); // undefined

Though, it will fail on

const a = f(null); // TypeError: Cannot match against 'undefined' or 'null'.

You can see how Babel transpiles the function to ES5 here.

Possible solutions

It can be avoided by arguments validation and preprocessing. In Python I could use decorator, but in JS we don't have them standardized, so it's not a good idea to use them yet. Though, assume we have a decorator checkInputObject, which makes needed checks and provides default values using given items list (or tree for the case of nested destructuring). We could use it in the following way without @ notation

const f = checkInputObject(['a'])(({ a }) => a);

It could look like this with @ notation

@checkInputObject(['a'])
function f({ a }) {
    return a;
}

Also I can make all needed actions in the function itself and only then use destructuring, but in this case I lose all advantages of function arguments destructuring (I'll not use it at all)

function f(param) {
    if (!param) {
        return;
    }
    const { a } = param;
    return a;
}

I can even implement some common function like checkInputObject in order to use it inside of the function

const fChecker = checkInputObject(['a']);
function f(param) {
    const { a } = fChecker(param);
    return a;
}

Though, using of additional code doesn't look elegant to me. I'd like to have not existent entities be decomposed to undefined. Assume we have

function f({a: [, c]) {
    return c;
}

It would be nice to get undefined in the case of f().

Question

Do you know any elegant and convenient way to make [nested] destructuring resistant to nonexistent nested keys?

My concern is following: seems like this feature is unsafe for using in public methods and I need to make a validation by myself before using it. That's why it seems useless until used in private methods.

Upvotes: 5

Views: 1407

Answers (4)

Estus Flask
Estus Flask

Reputation: 222493

The proper way to handle this in ES6 is to respect the way params are being processed by the language and shape API to fit it better, not in the opposite way.

The semantics behind destructured argument in fn(undefined) is that the argument will be replaced with default value, in the case of fn(null) means that nully argument won't be replaced with default value.

If data comes from the outside and should be conditioned/preprocessed/validated, this should be handled explicitly, not by destructuring:

function fn({ foo, bar}) { ... }

fn(processData(data));

or

function fn(data) {
  const { foo, bar } = processData(data);
  ...
}

fn(data);

The place where processData is supposed to be called is totally on developer's discretion.

Since proposed ECMAScript decorators are just helper functions with specific signature, same helper function can be used both with @ syntax and usual function calls, depending on the project.

function processDataDecorator(target, key) {
  const origFn = target[key];
  return target[key] = (...args) => origFn.apply(target, processData(...args));
}

class Foo {
  constructor() {
    this.method = processDataDecorator(this, 'method');
  }

  method(data) {...}
}

class Bar {
  @processDataDecorator
  method(data) {...}
}

And the way that the language offers since ES5 for default property values is Object.assign. It doesn't matters if data is undefined, null or other primitive, the result will always be an object:

function processData(data) {
  return Object.assign({ foo: ..., bar: ...}, data);
}

Upvotes: 2

trincot
trincot

Reputation: 350272

You could use try...catch and use that in a generic decorator function:

function safe(f) {
   return function (...args) {
       try {
           return f(...args);
       } catch (e) {}; // return undefined
   };
}

function f({ a } = {}) {
    return a;
}

f = safe(f);
console.log(f(null)); // -> undefined
console.log(f( { a:3 } )); // -> 3

Upvotes: 3

Bamieh
Bamieh

Reputation: 10906

I believe i can help you solve this problem by showing you a different way to look at it.

The problem you are facing is not about destructuring, because you will face it in any function that you are sending it different types that its expecting.

to solve such issue, ex. de-structuring, you can use type checking solution, such as Flow, which statically checks for types, hence it will fail at build time whenever you are sending wrong things to the function.

the essence of javascript is that its a dynamically types language, hence you might want the function to be able to work with any parameter sent to it, in this case, you can use dynamic type checking, the solutions are many.

the function parameters are more of an "agreement" to how you use the function, rather than accessing its parameters (after all you can just use arguments to access them), hence by sending different things you are violating this agreement, which isn't a good practice anyways, its better to have 1 function that does 1 thing, and accepts as few complicated parameters as possible.

Upvotes: 2

digitake
digitake

Reputation: 856

didn't you just answer yourself the question you ask?

I think default param is more elegant way for this.

function f({a: [,c]}={a:[undefined,undefined]}) {
    return c;
}

f();

Upvotes: -3

Related Questions