0

In a function of the form

function myFunc(param: any) {}

How to properly check if param can be spread? (...param)
How to know if an unknown variable can safely be spread for any type of spread (array/object)?

Alexandre Daubricourt
  • 3,323
  • 1
  • 34
  • 33
  • 1
    Spread into what? An array? An object? – Robby Cornelissen Mar 28 '23 at 07:58
  • If it's spreading into an object, then there's no limit, or at least not that I've heard of: `{ ...true, ...12, ...null, ...undefined, ...Infinity, ...NaN, ...(() => {}) } // No errors.` – InSync Mar 28 '23 at 08:01
  • To further what @RobbyCornelissen asked - what about spreading *into arguments*? On a technical level, that's the same as array spreading. However, from TypeScript level it isn't, since if you have a function `function fn(a: string, b: number){}` then spreading `fn(...[true, false])` into the call is incorrect. – VLAZ Mar 28 '23 at 08:11
  • Interestingly, while `{...undefined}` works as expected, `{...someUndefinedVariable}` *will* throw an error. – Brandon Gano Mar 28 '23 at 08:32
  • @BrandonGano not very interesting. Reading an *undeclared* variable ***always*** \* throws an error. Try `let x = y;` and you'll have the same error. A variable which is *declared* but has has `undefined` assigned to it (either implicitly or explicitly) will work without errors, however. \*except when used as `typeof someVariable` – VLAZ Mar 28 '23 at 08:49

1 Answers1

2

You can check if it's iterable using the Symbol.iterator property. If the variable is iterable, you can safely spread it.

Demo:

let arr = [1, 2, 3];
console.log(typeof arr[Symbol.iterator]); //function
let obj = { a: 1, b: 2, c: 3 };
console.log(typeof obj[Symbol.iterator]); //undefined
let unknownVar = [1, 2, 3];
if (unknownVar[Symbol.iterator]) {
  let newArr = [...unknownVar];
  console.log(newArr); //[1, 2, 3]
}
Mamun
  • 66,969
  • 9
  • 47
  • 59
  • 1
    This method falsely reports objects as non-spreadable and strings as spreadable. – Brandon Gano Mar 28 '23 at 08:13
  • 1
    @BrandonGano which is both actually correct if we're talking about *array spread* or *argument spread* (both of which are defined to work off the iterable protocol). – VLAZ Mar 28 '23 at 08:20
  • You're right. And strings *are* actually spreadable into an object, so my comment isn't even accurate for that case. – Brandon Gano Mar 28 '23 at 08:33
  • @BrandonGano array spread and object spread are different things. They just happen to have similar syntax `...` and name "spread". But are not really related. Hence why it's important for OP to clarify which one they mean. As I commented on the question, for TS there is also the case for argument spreading if you want to verify if something (probably a tuple) can be spread into a function call. As in, "would the members of this array/tuple satisfy the parameters defined in the function signature" which would instead be a type-level analysis that the compiler would do. – VLAZ Mar 28 '23 at 08:48
  • Right, I checked that one and it does not handle objects, I've edited my question the clarify that point. I'm looking for a way to check if anything can be spread, either object or array spread. I've used `Object.keys(any).length > 0` for that matter, but I do not feel confident about it – Alexandre Daubricourt Mar 28 '23 at 10:38
  • @AlexandreDaubricourt your question feels like [an XY problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem) at this point. What exactly do you need with spreading? What are you trying to do? `Object.keys(any).length` would tell you if an object has any keys but that *is not directly related to being object-spreadable*. It means an object has keys. However, an empty object is *also* object spreadable `{ ...{} }` is valid spreading and the result is `{}` as no properties exist. Even primitives can be spread `{ ...true }` and the result is the same. – VLAZ Mar 28 '23 at 12:17
  • While *array spreading* is a different thing altogether. Only iterables can be spread. But you probably shouldn't be trying runtime checks for those. In TS you can use the built-in `Iterable` to denote that you want iterables. Or just use arrays if you don't care about all iterables. Since `[ ...iterable ]` can also work with any object as long as it exposes an iterator. Not just arrays. If that's *not expected*, and you want to handle arrays, you should be handling arrays. – VLAZ Mar 28 '23 at 12:20
  • @VLAZ I'm coding a function that merge two object of any type, could be array, or objects, or if that unknown thing is not mergeable, just return the source. – Alexandre Daubricourt Mar 28 '23 at 12:27
  • @AlexandreDaubricourt Merging arrays is different to merging any iterable with array. For example if you just do `[...a, ...b]` that might work different to `a.concat(b)` (or the more neutral `[].concat(a, b)`) - consider [`@@isConcatSpreadable`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/isConcatSpreadable) - *should* you take that into account? Also, object spread is not really The One True Way to merge objects. See [`Object.assign`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign). – VLAZ Mar 28 '23 at 12:49
  • With all that said, it does seems you're asking for spreading (Y) when you should be asking for merging (X). And that [is a solved problem](https://stackoverflow.com/questions/27936772/how-to-deep-merge-instead-of-shallow-merge). You [don't even need to write it yourself](https://lodash.com/docs/#merge). – VLAZ Mar 28 '23 at 12:49