1

I am using an object to pass in arguments, because the function has many parameters, and JavaScript doesnt support named arguments like Python.

I would like to check that all the args have a value passed in. To do that I would like to loop over an array of the parameters, ['foo', 'bar', 'baz'] in the example below.

I would like to keep the code dry and not hardcode the list. I can access an object of the arguments with arguments[0], but how does one access the parameters ['foo', 'bar', 'baz'] programatically without hardcoding it?

function run({foo, bar, baz}) {
    const requiredKeys = ['foo', 'bar', 'baz']; // <- How to dynamically generate this
    for (const key in requiredKeys) {
        if (arguments[key] === null) throw `Please provide a value for ${key}`
    }
    console.log('done:', foo, bar, baz);
}
run({ foo: 1});

1
  • isn't typescript an option?) Commented Oct 15, 2022 at 19:25

3 Answers 3

2

Not everyone will see this as good practice, but you could use the source code of the function to extract the names of the properties of the parameter. Mozilla Contributors note:

Since ES2018, the spec requires the return value of toString() to be the exact same source code as it was declared, including any whitespace and/or comments

This makes parsing that string as reliable as the parsing system used.

For instance, you could use a decorator function that will extract the destructured parameter of the given function and return a function that will make the argument check:

// Decorator
function requireArgsFor(f) {
    // Extract (once) the destructered parameter definition. 
    // This assumes it is present as first parameter, and is not nested:
    const params = /\(\s*\{([^}]*)/.exec(f)[1].split(",").map(param => param.trim());
    return { // Object wrapper to ensure the decorated function gets the same name
        [f.name](obj) {
            const missing = params.find(param => !Object.hasOwn(obj, param));
            if (missing) throw TypeError(`Argument ${missing} is missing in call of ${f.name}`);
            return f(obj);
        }
    }[f.name]; 
}

function run({foo, bar, baz}) {
    console.log('run is running');
}

// Decorate the above function
run = requireArgsFor(run);
console.log(`calling ${run.name} with all required arguments...`);
run({ foo: 1, bar: 2, baz: 3 });
console.log(`calling ${run.name} with missing arguments...`);
run({ foo: 1 }); // Will trigger TypeError

You'd have to improve on the parsing part if you want to support more variations:

  • For supporting default values, like ({ foo=1, bar, baz })
  • For allowing "unnamed" parameters to precede this final parameter, like (a, b, {foo, bar, baz})
  • For supporting nested properties or arrays, like ({ foo: { bar }, baz: [first, ...rest] })
  • ...etc
Sign up to request clarification or add additional context in comments.

1 Comment

Wow you found a way to get it done. Impressive. I was hoping it would as easy as using some built in keyword, similar to the arguments builtin. Like you said this works, but code maintainability would suffer. Thank you!
1

Don't destructure the argument (at least not initially) - instead, declare an array of the required keys into the code, then iterate over the array.

function run(obj) {
    const requiredKeys = ['foo', 'bar', 'baz'];
    for (const key of requiredKeys) {
        if (obj[key] === null || obj[key] === undefined) {
            throw new Error(`Please provide a value for ${key}`);
        }
    }
    console.log('done');
}
run({ foo: 1});

A nicer way would be to use JSDoc or TypeScript to indicate to the consumer of run that it must be called with a particular type of object - that way mistakes can be caught while writing code, rather than only when it's running.

3 Comments

Then how does one use the requiredKeys array to desctructure obj into indivisual variables without duplicating the list of variables? PS I agree about types, but waiting for native JS types before using types, I keep the build step simple, allows me debug easily.
Depends what you're doing with the values. If you're doing the same thing with all the values, then use bracket notation and iterate over the requiredKeys, eg - for (const key of requiredKeys) { doSomething(obj[key]); } If you're not doing the same thing with all the values, and you want separate identifiers, then destructure after the verification is complete. Yes, it requires a bit of duplication, but not very much (and if you want separate identifiers, there's no way around it)
Thanks for the answer. If there is no way around then to me it makes sense to just hardcode const requiredKeys = ['foo', 'bar', 'baz']; and keep the code simpler.
0
const findMissingKeys = (obj, keys) => {
  const missingKeys = []
  keys.forEach(key => {
    if(!obj.hasOwnProperty(key) || obj[key] === undefined || obj[key] === null) {
      missingKeys.push(key)
    }
  });  
  return missingKeys;
}

You can do something similar to this.

Edit 1: Just saw that you need values to be passed as well. Changing the code accordingly

1 Comment

The question is how does one get the 2nd argument to your function. What problem are you trying to solve?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.