22

I just want to check that an object is a Map or Set and not an Array.

to check an Array I'm using lodash's _.isArray.

function myFunc(arg) {
  if (_.isArray(arg)) {
    // doSomethingWithArray(arg)
  }

  if (isMap(arg)) {
    // doSomethingWithMap(arg)
  }

  if (isSet(arg)) {
    // doSomethingWithSet(arg)
  }
}

If I were to implement isMap/isSet, what does it need to look like? I'd like for it to be able to catch subclasses of Map/Set if possible as well.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
nackjicholson
  • 4,557
  • 4
  • 37
  • 35

5 Answers5

31

The situation is similar to pre-ES5 methods to detect arrays properly and reliably. See this great article for the possible pitfalls of implementing isArray.

We can use

  • obj.constructor == Map/Set, but that doesn't work on subclass instances (and can easily be deceived)
  • obj instanceof Map/Set, but that still doesn't work across realms (and can be deceived by prototype mangling)
  • obj[Symbol.toStringTag] == "Map"/"Set", but that can trivially be deceived again.

To be really sure, we'd need to test whether an object has a [[MapData]]/[[SetData]] internal slot. Which is not so easily accessible - it's internal. We can use a hack, though:

function isMap(o) {
    try {
        Map.prototype.has.call(o); // throws if o is not an object or has no [[MapData]]
        return true;
    } catch(e) {
        return false;
    }
}
function isSet(o) {
    try {
        Set.prototype.has.call(o); // throws if o is not an object or has no [[SetData]]
        return true;
    } catch(e) {
        return false;
    }
}

For common use, I'd recommend instanceof - it's simple, understandable, performant, and works for most reasonable cases. Or you go for duck typing right away and only check whether the object has has/get/set/delete/add/delete methods.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Why not use `Object.prototype.toString.call(o) == '[object Set]'`? Is there some case when that would fail, other than someone overriding `Object.prototype.toString`? (I don't think an overridden `Object.prototype.toString` is worth worrying about - it's a bad practice that would break lots of existing libraries.) – Matt Browne Mar 04 '17 at 13:49
  • @MattBrowne `Object.prototype.toString` is equivalent to using the `Symbol.toStringTag` method - you don't need to overwrite the global, it can be messed with on a per-object basis. – Bergi Mar 04 '17 at 20:16
8

You can use the instanceof operator:

function isSet(candidate) {
  return candidate instanceof Set;
}

If the candidate object has Set.prototype in its prototype chain, then the instanceof operator returns true.

edit — while the instanceof thing will work most of the time, there are situations in which it won't, as described in Bergi's answer.

Pointy
  • 405,095
  • 59
  • 585
  • 614
  • Will that work even if the set is a subclass of `Set`? I should've specified, that as a concern. – nackjicholson Apr 28 '15 at 16:31
  • 1
    @nackjicholson well that depends on how you're creating the subclass. The `instanceof` operator returns `true` if the `.prototype` of the constructor is in the object's prototype chain. – Pointy Apr 28 '15 at 16:33
  • 1
    @nackjicholson: As long as you do `class MySet extends Set`, yes. – Felix Kling Apr 28 '15 at 16:37
  • 1
    Note that if you're writing code for a library that will be redistributed, it would be good to check first if `window.Set` is defined (unless your library includes a shim for older browsers). (Or to make it work on the server too, you could check `typeof Set != 'undefined'`). – Matt Browne Mar 04 '17 at 13:44
  • @MattBrowne yes in fact I myself found bugs involving `instanceof` when dealing with object passed between ` – Pointy Mar 04 '17 at 13:49
  • Cool. I'm updating my open-source `deepCopy()` function and thinking I should use `Object.prototype.toString.call(o) == '[object Set]'`. I just commented about using `Object.prototype.toString` on Bergi's answer... – Matt Browne Mar 04 '17 at 13:51
  • @MattBrowne it's regrettable that whoever drove getting `Array.isArray()` into the spec didn't also get `Set.isSet()` etc in there also. – Pointy Mar 04 '17 at 13:54
  • 1
    Yes. Actually it's worse in this case because unlike arrays, Sets are only in the most recent spec. It's common to use a shim for older browsers, and I just realized that the `Object.prototype.toString` check wouldn't work for those. I may post an answer here with what I ultimately land on. – Matt Browne Mar 04 '17 at 14:15
1

You can simply use:

export function isMap(item) {
  return !!item && Object.prototype.toString.call(item) === '[object Map]';
}

export function isSet(item) {
  return !!item && Object.prototype.toString.call(item) === '[object Set]';
}

Unless the prototype of this method was overridden

Melchia
  • 22,578
  • 22
  • 103
  • 117
  • Is the boolean check strictly needed? Seems to work with falsy values without. – Brett Zamir Apr 11 '20 at 04:34
  • Yes it's needed because object.toString is called on item if it's undefined or null it will throw error – Melchia Apr 11 '20 at 15:22
  • In Chrome and Firefox, for `Object.prototype.toString.call(null)` I get `"[object Null]"` and "[objectUndefined]" for a call on `undefined`. – Brett Zamir Apr 12 '20 at 00:27
0

The following method is used in Vue source code. You can borrow the idea from here.

export const isMap = (val: unknown): val is Map<any, any> =>
  toTypeString(val) === '[object Map]'
export const isSet = (val: unknown): val is Set<any> =>
  toTypeString(val) === '[object Set]'


export const objectToString = Object.prototype.toString
export const toTypeString = (value: unknown): string =>
  objectToString.call(value)

It is easy to understand. After converting the object to string. Then you just need to check the value of string if it is '[object Map]' or '[object Set]'

I highly recommend front-ender read the Utility functions in the source code. You can always find the best practice.

Here is the link to Vue source: https://github.com/vuejs/vue-next/blob/master/packages/shared/src/index.ts

you can find the code I quoted here.

Steven Chen
  • 101
  • 1
  • 4
0

Or check the prototypes:

Object.getPrototypeOf(i) === Map.prototype
vimuth
  • 5,064
  • 33
  • 79
  • 116