30

I want to validate an object that has three Boolean properties, of which one, and only one, must be true. What is the best way to check this?

I came up with this:

var t1 = obj.isFirst  ? 1 : 0,
    t2 = obj.isSecond ? 1 : 0,
    t3 = obj.isThird  ? 1 : 0;

var valid = (t1 + t2 + t3) === 1;

This works fine, but it feels a bit like a hobbyists solution :-)

Is there a better way of doing this? Using XOR (^) for example?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Michiel
  • 4,160
  • 3
  • 30
  • 42
  • 5
    `obj.isFirst + obj.isSecond + obj.isThird === 1`, assuming it is certain they are booleans and not something different. You can add explicit conversion if you like (e.g. `Number(obj.isFirst)`), but javascript also implicitly converts. – ASDFGerte Jul 17 '18 at 14:20
  • Related: [Why is there no logical xor in JavaScript?](https://stackoverflow.com/q/4540422/578288), whose answers include some alternatives. – Rory O'Kane Jul 17 '18 at 14:24
  • 11
    'feels a bit like a hobbyists solution' I can assure you that the _pros_ write code that looks exactly like this. Stop worrying about how your code looks, nobody is going to be looking at it, make sure the compiler can read it and that it does what it should. – Adam H Jul 17 '18 at 14:33
  • 1
    @ASDFGerte I didn't know one could add up booleans like that, that will make my code a bit cleaner. I will use that – Michiel Jul 17 '18 at 14:37
  • 6
    @Michiel as someone who could potentially have to come to maintain the code you are writing, please don't do that. It does nothing but make your code harder to read and maintain in the future and offers 0 benefits. – Adam H Jul 17 '18 at 14:43
  • 2
    [Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%.](https://en.wikiquote.org/wiki/Donald_Knuth) as written by a recipient of the Turing award. There really is nothing to optimise here. – Liam Jul 17 '18 at 14:53
  • In hindsight, i have minor concerns about my posted comment. Although the conversion from true/false to 1/0 has been around for ages (at least early C times), implicit conversions should usually be avoided. Also, it is still not what i would consider instantly clear when being read. However, while bambam's answer may be slightly better (no implicit conversions and perhaps a bit better in readability), i still don't look at it and think "that's it!". Maybe this is just as good as it gets, without writing a function for it that has a descriptive name, or a comment. – ASDFGerte Jul 17 '18 at 14:56
  • 4
    @Liam that quote refers to people concerning themselves with execution time. It is not applicable here, as this question is about readability, avoidance of errors (by being more obvious, better to read), and being easier to refactor. – ASDFGerte Jul 17 '18 at 15:03
  • 4
    Just a note, not an answer: I think it's a wrong model. It's better to have: `obj.position = (1|2|3)` – Luca Rainone Jul 17 '18 at 15:10
  • If you can only have one position, then you should store the position instead. – the_lotus Jul 17 '18 at 15:16
  • I think your current implementation is the most readable/maintainable/extensible. – Woohoojin Jul 17 '18 at 15:55

7 Answers7

20

You could filter the Object.values and check if the length is 1

let obj = {
    a: true,
    b: false,
    c: false
};
let oneTrue = Object.values(obj).filter(e => e === true).length === 1;
console.log(oneTrue);

As @deceze commented

Should there be more properties in the object: [obj.a, obj.b, obj.c].filter(Boolean)...


Note that in your case, it might make sense to add a method to your object that does the check for you

let obj = {
    a: false,
    b: false,
    c: false,
    isValid() {
        return this.a + this.b + this.c === 1;
    }
};

console.log(obj.isValid());
baao
  • 71,625
  • 17
  • 143
  • 203
  • 4
    Should there be more properties in the object: `[obj.a, obj.b, obj.c].filter(Boolean)`… – deceze Jul 17 '18 at 14:27
  • 1
    Thanks @deceze added it as a quote to the answer. Good addition – baao Jul 17 '18 at 14:29
  • 1
    Yes, my object has other properties, but I see this would definitely work otherwise – Michiel Jul 17 '18 at 14:33
  • 1
    You can just take them like commented by deceze @Michiel. Simply replace a,b,c with your property's names and filter them. – baao Jul 17 '18 at 14:34
  • @Michiel Note that, if your object has only those 3 boolean values and any other properties that are **not `Boolean` type**, the original answer will work either. – baao Jul 17 '18 at 14:56
  • A different insight to this is, it does not work with: `let obj = { a: 'false', b: false, c: false } ` – tika Jul 17 '18 at 15:04
  • 2
    @tika right, corrected – baao Jul 17 '18 at 15:33
9

You can use array.reduce:

var obj = {
    a: true,
    b: false,
    c: false
};
var res = Object.values(obj).reduce((m, o) => m + o) === 1;
console.log(res);
Faly
  • 13,291
  • 2
  • 19
  • 37
7

Use:

obj.isFirst + obj.isSecond + obj.isThird === 1

Assuming it is certain that the values are Booleans and not something different. You can add explicit conversion if you like, like this:

Number(obj.isFirst) + Number(obj.isSecond) + Number(obj.isThird) === 1

But JavaScript also implicitly does this conversion for you.

(This is based on ASDFGerte’s comment.)

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Rory O'Kane
  • 29,210
  • 11
  • 96
  • 131
  • 2
    If you really want to add booleans, you can also convert them with `!!`. And since `true` is treated as a numeric `1` and `false` as `0` in Javascript, `!!obj.isFirst + !!obj.isSecond + !!obj.isThird` accomplishes the same. Alternatively, `+` casts to a number: `+obj.isFirst + +obj.isSecond + +obj.isThird`, and now my head hurts. – Bucket Jul 17 '18 at 14:55
  • @Bucket using `!!` on a boolean accomplishes exactly nothing. – ASDFGerte Jul 17 '18 at 14:59
  • Check this: `Number(1) + Number("true") + Number(true)` vs `Boolean(1) + Boolean("true") + Boolean(true)` – tika Jul 17 '18 at 15:00
  • @ASDFGerte you're correct. But using `!!` on anything else can be quite useful. – Bucket Jul 17 '18 at 15:29
  • 2
    @ASDFGerte I believe the purpose of `!!` is in case obj.isFirst is already a number equal to `5`, for example. `if(5) { console.log("5 is true"); }` will print out to the console. so will `if(!!5)`, but now the argument sent to `if` is a boolean `true` instead of the number `5`, so adding will work as expected now. – Woohoojin Jul 17 '18 at 15:58
  • `!!foo` might be more readable as `!(!foo)`. If `foo` is truthy, `!foo` will return `false`, (and vice-versa), then the outer `!` will invert this value _again_, effectively casting `foo` to a boolean. JS booleans have their quirks, however: `!!0 === false`, `!!"0" === true`, and `!![] === true`. – Bucket Jul 18 '18 at 14:04
4

Going at the problem from the other way around, it looks like you're trying to model something that can have 3 possible states. Don't represent it as 3 booleans, represent it as a variable with 3 possible values:

const STATES = ['STATE_A', 'STATE_B', 'STATE_C'];
let value = 'STATE_A';

If you want to use one of the booleans:

doSomethingDependingOnBooleanC(value === `STATE_C`);

If you want to validate that value holds what you expect (one of your 3 possible states):

const isValid = STATES.includes(value);
const isValidInOlderBrowsers = STATES.indexOf(value) !== -1;
Tibos
  • 27,507
  • 4
  • 50
  • 64
  • 2
    To take this a step further you can use object properties. `const STATES = Object.freeze({STATE_A: 1, STATE_B: 2, STATE_C: 3});` `let value = STATES.STATE_A;`. – Yay295 Jul 18 '18 at 05:35
  • @Yay295 That is exactly how i would do it, perhaps with Symbol s instead of numbers for the values, but i didn't want to complicate the answer with how to define an enum in JS. My answer is "simplify your data model", not "use this code that works". – Tibos Jul 18 '18 at 07:11
2

You can do this in one line, but it's not easy to read. But if obfuscation is your goal, read on.

Since obj.isFirst, obj.isSecond, and obj.isThird are all either 0 or 1, you can use the bitwise XOR operator ^ between them:

(obj.isFirst ^ obj.isSecond ^ obj.isThird) ^ (obj.isFirst && obj.isSecond && obj.isThird)

We need to XOR against obj.isFirst && obj.isSecond && obj.isThird because obj.isFirst ^ obj.isSecond ^ obj.isThird evaluates to true.

Bucket
  • 7,415
  • 9
  • 35
  • 45
  • So, this means the OP has to hard-code all their properties, _twice_. – Cerbrus Jul 17 '18 at 14:30
  • But you still have to define `t1`,`t2`, and `t3` first. That counts as part of the answer, making it four lines instead of the one line you show. – Rory O'Kane Jul 17 '18 at 14:30
  • 2
    Even if this works, it seems overly obfuscated/hard to grok. At the very least it requires an extra line of commenting to explain what it does, which somewhat negates the purpose. – deceze Jul 17 '18 at 14:32
  • @deceze when it comes to OP's question, finding the "best way" is often ambiguous, especially since it's implied he's asking for a non-hobbyist answer. There are many correct ways to do this, this is simply one that can be contained in a single line at the cost of obfuscation. Granted, nobody who writes code professionally should ever try to create unreadable code, so I accept that this isn't the best answer, albeit (so far) the shortest one. – Bucket Jul 17 '18 at 14:32
  • 3
    @Bucket Readability and maintainability are very much hallmarks of *non-hobbyist code* if you ask me… – deceze Jul 17 '18 at 14:35
  • 2
    Agreed, see ninja-edit – Bucket Jul 17 '18 at 14:37
2

How about:

var valid =  (obj.isFirst + obj.isSecond + obj.isThird) === 1;

Note: The return of a sum is an integer and it translates true to 1 and false to 0. So you do not actually need to to boolValue ? 1 : 0

See this result set for reference:

console.log(true + false + false)  // 1
console.log(false + false + false) // 0
console.log(false + false + true)  // 1
console.log(1 + false + 0)         // 1
console.log(false + 1 + 1)         // 2

Also let me point out, if you want explicit conversion and your data is string, you may fall into this issue. So, I would rather use Boolean than Number.

var a = "true",
    b = false,
    c = false;

var valid = Number(a) + Number(b) + Number(c);
console.log(valid); // NaN

var a = "true",
    b = false,
    c = false;

var valid = Boolean(a) + Boolean(b) + Boolean(c);
console.log(valid); // 1
tika
  • 7,135
  • 3
  • 51
  • 82
1

Wrap it in a function named 'ensureOnlyOnePropertyIsTruthy'. Have it take a list of property names along with the object and stuff whatever ugly, performant code you want inside. Nobody will have to read the internals because your function gives them the only context necessary.

aaaaaa
  • 1,233
  • 13
  • 24
  • 1
    I would do that is there were more places in my code where I would use that function. Creating a funtion which is only used once, seems a bit overdone to me. – Michiel Jul 18 '18 at 07:32
  • @Michiel To me the functionality seems it would be common enough to stuff it in a utility library that you can consume in other projects. Also, functions don't have to be reused. Even if it is used only once - it still provides much better readability than the alternatives. – aaaaaa Jul 18 '18 at 18:58
  • By wrapping the code in a function you're providing your reader with a concise 'what' is being done without the 'how'. If they care about 'how', they can dig into your function. I use this paradigm heavily in my own code - and I highly doubt you only write functions that are re-used. That would cause for some very long, obscure functions. – aaaaaa Jul 18 '18 at 19:11