vue: Minor bug with Boolean props casting: undefined => false

Vue.js version

2.1.10

Reproduction Link

https://jsfiddle.net/nymhjosc/

Steps to reproduce

Simply omit a Boolean prop and the value will be casted to false.

What is Expected?

I’d expect to have an undefined property just to know that the user didn’t provide it (since it is not a required prop). Basically, I would expect both myProp and missingProp in the previous example to be the same.

What is actually happening?

When the prop is not provided its content is casted to false. Also, some other values like '' are casted to true (somewhat related to #4538) but I guess this is intended. I’m using default: undefined for now.

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 7
  • Comments: 28 (7 by maintainers)

Commits related to this issue

Most upvoted comments

Although I highly respect all of your opinions and knowledge, I would like to present a different view.

As a developer, I may want to know if a Boolean prop was defined or not.
Here are two examples:

  1. As a component developer for external use, I may want to build some logic to determine if the property was defined or not for various internal checks and robustness. And, it is not realistic to impose the use of :my-bool=“undefined”, especially when trying to make a component robust and flexible. Fortunately, there is a workaround that does not impose external changes, that is to explicity set the default to undefined: boolProp: {type: Boolean, default: undefined}

  2. A multi-value prop that includes a boolean value may be desired. For example happyIcon: [Boolean, String]. This simplifies the usage of the component (which assumes happy by default) with the following example scenarios for the prop: a) false: no icon displayed b) undefined, defined or true: display the default icon c) string: custom icon name

However, since vue forces undefined to false, I cannot manage case b since I cannot observe the undefined state and would be forced to add a second prop, thus requiring two props (Boolean and String) to do something that could be done with one. Additionally, I cannot just ask the user to pass a string “false” or “true” in the string since this could be an icon name.

Since undefined is fasly, there is no negative impact in keeping it, however, there is a loss of functionality by removing it.

As an additional irritant, if I define a Boolean and String prop with undefined as a default and the prop is defined, the component receive an empty string instead of the expected true value. (Maybe I should submit this as a seperate issue?)

    bsu: { 
      type: [Boolean, String], 
      default: undefined
    },

See the following jsfiddle for the examples: https://jsfiddle.net/realcarbonneau/daxs9722/

The boolean casting follows the same rule of a boolean attribute: presence of any value are casted to true, absence means false.

Personally I think it’s a bad idea to differentiate a “3rd state” for boolean props.

Yup this issue is quite prominent when building radix-vue, where we expose the Props & Emits type for dev to build wrapper component around it. It makes it super hard to build wrapper components, as I cant expect the dev to always define the props with withDefaults and manually set those boolean props to undefined.

Also, by casting the boolean to false really surprised me as I’m expecting the type to be undefined (explained by above comment nicely). I gotta agree with @matthew-dean in terms of violating Law of Least Surprise.

To workaround this issue, I’ve created a useForwardProps, showcasing on shadcn-vue and unovis/vue.

This is indeed a pain right now…

I’m just getting started with Vue and this behaviour was definitely a surprise to me.

I’m using the composition API with the defineProps<{ open?: boolean | undefined }>() macro, and I expected open to be undefined if omitted.

It’s weird that props.open is typed as boolean instead of boolean | undefined, but I guess I’ll just have to get used to that footgun.

I just had this same issue today.

If I define a prop as someProp?: string and it will be undefined, when no attribute is passed to the prop, why doesn’t someProp?: boolean behave the same way??

Worst part is that TypeScript understands it exactly as that - boolean or undefined, but in reality it’s always boolean.

This is still a weird part of Vue’s API, and violates the Law of Least Surprise.

Take this statement by @yyx990803:

The boolean casting follows the same rule of a boolean attribute: presence of any value are casted to true, absence means false.

Personally I think it’s a bad idea to differentiate a “3rd state” for boolean props.

All attributes in HTML are this way, not just booleans. Open up Chrome, inspect an element, and select Properties. You will see no undefined values! If it’s a string, it will default to "". If it’s a number, it will default to 0 or -1 (depending on what they represent). If it’s a boolean, it’s true or false.

It’s absolutely counter-intuitive to treat booleans differently. Either all Vue properties should have default values, by default, or none should.

More importantly, though, from a component perspective, is this statement by @realcarbonneau:

As a developer, I may want to know if a Boolean prop was defined or not

Because these are developer-to-developer JavaScript and TypeScript interfaces, while it may seem a “bad idea to differentiate a 3rd state for boolean props” to some, this is a core staple of the JavaScript / TypeScript language itself. i.e. developers want to specify a behavior for when a property is not set at all. We can call it a “3rd state”, but it’s a 3rd state in the way that omitting an object property or omitting a function’s argument is a 3rd state for that variable / value. That is, all types in JavaScript / TypeScript have third states (except in the latter case when null or undefined is explicitly omitted). So there is developer value in defaulting to undefined. In fact, we know there is, because every other type defaults to undefined.

In fact, this gets even more off-kilter when TypeScript support was increased for Vue, and you have interfaces like this:

interface Props {
  name?: string
  isCool?: boolean
}

In TypeScript, both of these properties are defined as optional, which means automatically that their respective values are string | undefined and boolean | undefined. Despite the syntax being exactly the same, Vue’s TypeScript implementation handles the interface inconsistently, defining name as string | undefined (instead of string) and defines isCool as boolean.

So, there’s little rational reason why booleans not only behave inconsistently from an HTML perspective, and not only behave inconsistently from a Vue perspective, but also behave inconsistently from a JavaScript and TypeScript perspective. It really is a complete oddity in this legacy behavior.

@dondevi I changed it to this, because props is not always a guaranteed object under type:

      for (const [key, prop] of Object.entries(this.$props)) {
        const type = this.$.type?.props?.[key]
        if (type && prop === false && !('default' in type)) {
          /**
           * @hack `props` is readonly, use `props.__v_raw` to get a mutable `Proxy`
           */
          this.$props.__v_raw[key] = undefined
        }
      }

There might also be an opportunity to optimize for re-mounting, because the props keys will not change between instances, so you technically only have to iterate boolean keys, but I couldn’t figure out how to get the original component

I’d vote for undefined as default value for 2 reasons:)

  • other types (number, string and object) default to undefined. So the boolean should not be an exception.
  • if a prop can be marked as optional (not required), it sounds for me like a nullable prop. And I think it should be possible to know if it has been omitted, instead of changing it to any valid value. This could disguise errors, I think. But I understand the other arguments as well.

However, the workaround from frandiox works well (explicitely specifying an undefined or null as default value).

aBooleanProp: {
    type: Boolean,
    default: undefined // to be consistent with other types
}

Is that the recommended and save way to handle optional boolean props? Or should I use a wrapper object for optional types as mrwiner suggested?