0

I need to pass an argument, that is either an array of one type or the type on its own, to a function that requires the only argument be an array:

// myFunction throws an error if the first argument is not an array
const foo = typeOrArrayOfType => myFunction([...typeOrArrayOfType, oneDefault]);

However if I do this:

const anArray = ['oneItem', 'twoItem'];
const notAnArray = 'oneItem';
const nonIterator = 300;
const oneDefault = 20;

const foo = typeOrArrayOfType => console.log([...typeOrArrayOfType, oneDefault]);

foo(anArray) // prints array items
foo(notAnArray) // spreads the string
foo(nonIterator) // error

It either works, spreads the string into characters or breaks entirely.

How can I flexibly take arguments that may or may not be an array?

I know about Array.isArray but I don't want helper function or conditionals if possible. I don't care about nested arrays, but if they behave differently it would be worth a note about it.

AncientSwordRage
  • 7,086
  • 19
  • 90
  • 173
  • 2
    good ol [Array#concat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/concat) deals with both arrays and non-arrays `[].concat(typeOrArrayOfType, oneDefault)` just not with iterables. – Thomas Feb 01 '22 at 11:59

3 Answers3

1

Create an array only if necessary

Declare a simple helper that wraps non-array values in an array or leaves the original intact:

const toArray = data =>
  Array.isArray(data)
    ? data
    : [data];

Example:

const anArray = ['oneItem', 'twoItem'];
const notAnArray = 'oneItem';
const nonIterator = 300;
const oneDefault = 20;

const toArray = data =>
  Array.isArray(data)
    ? data
    : [data];

const foo = typeOrArrayOfType => console.log([...toArray(typeOrArrayOfType), oneDefault]);

foo(anArray) // works
foo(notAnArray) // works
foo(nonIterator) // works

Convert everything to new arrays

The above has a slight weakness - it returns the original array in some cases. Which means that mutations might affect it:

const toArray = data =>
  Array.isArray(data)
    ? data
    : [data];

function test(input) {
  const arrOutput = toArray(input);
  arrOutput.push("world");
  
  return arrOutput;
}

const arrInput = ["hello"];

const output = test(arrInput);
console.log(output);   // [ "hello", "world" ]
console.log(arrInput); // [ "hello", "world" ]

To handle this, you could copy every array uniformly using Array#concat() - if given an array, it will produce a new array with a copy of its contents (only one level), if given non-array it will create a new array with the argument(s) as item(s):

const toArray = data =>
  [].concat(data);

Example:

const anArray = ['oneItem', 'twoItem'];
const notAnArray = 'oneItem';
const nonIterator = 300;
const oneDefault = 20;

const toArray = data =>
  [].concat(data);

const foo = typeOrArrayOfType => console.log([...toArray(typeOrArrayOfType), oneDefault]);

foo(anArray) // works
foo(notAnArray) // works
foo(nonIterator) // works

Example which does not have a problem with shared arrays:

const toArray = data =>
  [].concat(data);

function test(input) {
  const arrOutput = toArray(input);
  arrOutput.push("world");
  
  return arrOutput;
}

const arrInput = ["hello"];

const output = test(arrInput);
console.log(output);   // [ "hello", "world" ]
console.log(arrInput); // [ "hello" ]

This might be even simpler to use, since it removes the need to spread items. The .concat() method already creates a new array and accepts variable number of arguments, it can be used directly as a way to create a new array with extra items:

const anArray = ['oneItem', 'twoItem'];
const notAnArray = 'oneItem';
const nonIterator = 300;
const oneDefault = 20;

const foo = typeOrArrayOfType => console.log([].concat(typeOrArrayOfType, oneDefault));

foo(anArray) // works
foo(notAnArray) // works
foo(nonIterator) // works

Note: the @@isConcatSpreadable well-known symbol property can affect how Array#concat() works. When set to true then concatinating the object will be "flatten" similar to how arrays are. This will work on any array-like. Conversely setting the property to false will prevent .concat() from spreading the object:

//make an object string
const str = new String('bar');
//make it spreadable
str[Symbol.isConcatSpreadable] = true;

console.log([].concat(str));


//a spreadable array-like:
const arrayLike = { 
  0: "h", 1: "e", 2: "l", 3: "l", 4: "o", 
  length: 5,
  [Symbol.isConcatSpreadable]: true
};

console.log([].concat(arrayLike));

//non-spredable array:
const arr = ["x", "y", "z"];
arr[Symbol.isConcatSpreadable] = false;

console.log([].concat(arr));
.as-console-wrapper { max-height: 100% !important };
VLAZ
  • 26,331
  • 9
  • 49
  • 67
  • Your example of `[].concat(data)` is the only viable alternative I can see here, as I could inline it. as `console.log([...[].concat(typeOrArrayOfType), oneDefault]);` right? – AncientSwordRage Feb 01 '22 at 12:46
  • Yes, you can. The function was just a level of abstraction, so both solutions act the same. But inlining it is the same. In fact, you can save up even more by doing `[].concat(arrayOrNotArray, extra, items, here)` which will create a new array with three extra items in it. No need to spread anything: `console.log([].concat(typeOrArrayOfType, oneDefault))` – VLAZ Feb 01 '22 at 12:48
  • I find that slightly more confusing to look at, but it's a *very* good answer. I think that's because if I hadn't looked up the function, I'd have assumed `[].concat('bar')` would result in `['b', 'a', 'r']`. Clearly I need to go refresh my Array knowledge. – AncientSwordRage Feb 01 '22 at 13:26
  • Technically, it *might*. In the vast majority of cases, with `.concat()` you only deal with either arrays or not. However, it's technically possible to affect the behaviour via the [`@@isConcatSpreable`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/isConcatSpreadable) property. If set to `true`, then the object will be "flattened" similar to arrays when passed into `concat()`: [Example](https://jsbin.com/vamuvaw/1/edit?js,console). Primitives cannot be affected but if you have any custom object, it might be. But chances very low to encountering it. – VLAZ Feb 01 '22 at 14:19
  • That might be cool to note! If you do, the docs are [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/isConcatSpreadable) – AncientSwordRage Feb 01 '22 at 14:25
0

You should handle the variable by testing if it's an array with Array.isArray():

e.g.

const anArray = ['oneItem', 'twoItem'];
const notAnArray = 'oneItem';
const nonIterator = 300;
const oneDefault = 20;

const foo = typeOrArrayOfType =>
  console.log(Array.isArray(typeOrArrayOfType)
  ? [...typeOrArrayOfType, oneDefault]
  : [typeOrArrayOfType, oneDefault]);

foo(anArray) // prints array items
foo(notAnArray) // spreads the string
foo(nonIterator) // error

It would take very little effort to factorise the check into a utility function, which other answers do.

AncientSwordRage
  • 7,086
  • 19
  • 90
  • 173
Keith
  • 22,005
  • 2
  • 27
  • 44
  • I don't like that you need to re-write `[...typeOrArrayOfType, oneDefault]` with or without the spread. That's basically what I was trying to avoid. If I get extra defaults I need to keep *both* up to date. – AncientSwordRage Feb 01 '22 at 12:41
  • Downvoted you question, as that's not what you asked for. It takes very little effort to factorise the check into a utility function like @VLAZ did, and in some respects that's a different question. – Keith Feb 01 '22 at 13:32
  • I've updated my question to be clearer. But 'how to handle a variable that might be an array or not' is still very much my question, and doing so inline should have been clear from my examples. I was not asking 'How do I test if an object is an array'. – AncientSwordRage Feb 01 '22 at 13:36
  • @AncientSwordRage `How can I flexibly take arguments that may or may not be an array?` Yes you was.. And @VLAZ is doing exactly the same, he's just factored it out into a function. So his answer is no different to mine. – Keith Feb 01 '22 at 13:38
  • 1
    I've made an edit, to make this a better answer to my question, and reversed my downvote. Also note, the reason I upvoted VLAZ is because of `[].concat()`. – AncientSwordRage Feb 01 '22 at 13:39
  • @AncientSwordRage Yes, the `[].concat()` is nifty solution.. :) – Keith Feb 01 '22 at 13:47
-1

You can wrap it in an array and then use .flat() before spreading*

I've not seen this anywhere else on Stack Overflow.

This means that you always end up spreading an array:

const anArray = ['oneItem', 'twoItem'];
const notAnArray = 'oneItem';
const nonIterator = 300;
const nestedArray = ['lets', ['go', 'nest', ['arrays']]];
const anIterator = new Set([1,3,5,3]);
const oneDefault = 20;

const foo = typeOrArrayOfType => console.log([...[typeOrArrayOfType].flat(), oneDefault]);

foo(anArray) // nested array items is flattened one level
foo(notAnArray) // wrapped string doesn't get flattened
foo(nonIterator) // wrapped number doesn't get flattened
foo(nestedArray) // only flattens one level, added to show how .flat(works)
foo(anIterator)  // this does not work, and results in {}

N.B. Using .flat() only goes down one level by default (unlike, say lodash.flattenDeep or some other libraries). You could optionally supply a number, but that is out of the scope of the question.

const foo = (typeOrArrayOfType, depth=1) => console.log([...[typeOrArrayOfType].flat(depth), oneDefault]);

And not useful for the situation in the question.


* As an aside, some of the other answers use [].concat(variable) which to me is as clear or slightly less clear, but is somewhat more performant, as per this jsbenchmark without spreading or with spreading.

AncientSwordRage
  • 7,086
  • 19
  • 90
  • 173