-
-
Notifications
You must be signed in to change notification settings - Fork 33.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Minor bug with Boolean props casting: undefined => false #4792
Comments
Since you give the prop a Boolean type, it's normal to have a |
@posva Sometimes I use it for modals so parent can control the state of modal with prop a value and if prop is |
@znck Yeah, I cannot remind where I read something similar, but being able to explicitly pass |
Passing |
I don't think so, because we're asking for a |
@znck yeah, but what is the purpose of EDIT: Nevermind, the issue is not about |
@posva Honestly, the same way a missing And the other thing I pointed out
is also kind of weird to me. In JavaScript an empty string is a falsy value. |
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. |
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.
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?)
See the following jsfiddle for the examples: https://jsfiddle.net/realcarbonneau/daxs9722/ |
ensure boolean props is undefined when it is undefined cf: - vuejs/vue#4792 - https://jsfiddle.net/realcarbonneau/daxs9722/ True for vue 1 and vue 2
The main use case I can see for having a third state for a boolean is HTML attributes:
|
I'm just getting started with Vue and this behaviour was definitely a surprise to me. I'm using the composition API with the It's weird that |
Chiming in years later to say that this confounded me as well. Since we are using https://github.com/gcanti/fp-ts, I opted to make the prop an Option type, which essentially provides the desired functionality.
Doubt it's worth installing this package just for this but it does the trick. |
I'd vote for undefined as default value for 2 reasons:)
However, the workaround from frandiox works well (explicitely specifying an undefined or null as default value).
Is that the recommended and save way to handle optional boolean props? |
This is still a weird part of Vue's API, and violates the Law of Least Surprise. Take this statement by @yyx990803:
All attributes in HTML are this way, not just booleans. Open up Chrome, inspect an element, and select 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:
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 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 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. |
I just had this same issue today. If I define a prop as Worst part is that TypeScript understands it exactly as that - boolean or undefined, but in reality it's always boolean. |
It's a part of the Vue API that makes zero rational sense. In JavaScript (and TypeScript), Booleans are not a different kind of primitive from other primitives. And, yes, it violates the explicit definition of the type, so it's 100% a TypeScript bug, even if Vue maintainers want to insist it's not a Vue bug. |
Yup this issue is quite prominent when building Also, by casting the To workaround this issue, I've created a useForwardProps, showcasing on shadcn-vue and unovis/vue. This is indeed a pain right now... |
Here is a hack to solve the problem: function isPlainObject(value) {
return value?.constructor === Object;
}
app.mixin({
beforeCreate() {
/**
* Fix default value to `undefined` of Boolean props when inheriting
*/
Object.keys(this.$props).forEach((key) => {
const prop = this.$props[key];
const type = this.$.type.props[key];
if (isPlainObject(type) && prop === false && type.default === undefined) {
/**
* @hack `props` is readonly, use `props.__v_raw` to get a mutable `Proxy`
*/
this.$props.__v_raw[key] = undefined;
}
});
},
}); 💡 A new version see below. |
Make Runtime Props Validation Optional Please 🙏 Instead show "Vite Error Overlay" on screen With Casting Props in runtime we would never have Complex Types or Entire Props Type Object support for |
@dondevi Thank you SO MUCH for fixing this! EDIT: Your code comment says "Fix default value to |
Because of the |
@dondevi I changed it to this, because props is not always a guaranteed object under 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 |
@dondevi Wait a minute... I think your logic is flawed.... If I have a component like this: <script setup lang="ts">
defineProps<{
bool?: boolean
}>()
</script> ... then it has no |
@dondevi I think this comment actually ends up being the better workaround. Then your component can be like (note capital <script setup lang="ts">
defineProps<{
bool?: Boolean
}>()
</script> As long as Vue never breaks this behavior, then I think that's a suitable workaround that should be more publicized. |
@matthew-dean Thanks for pointing it out, it won't cover all scenarios as it's a hack to attract good ideas. The logic has been updated with |
@matthew-dean A better solution: /**
* @hack Prevent vue's behavior of casting boolean default value to false
* @since vue 3.3+
*
* @see {@link vuejs/core/packages/runtime-core/src/componentProps.ts:484#resolvePropValue} - boolean casting
*/
app.mixin({
beforeCreate(this: ComponentPublicInstance) {
const { props, type, vnode } = this.$;
const types = Object.entries(type.props || {}) as Array<[string, any]>;
for (const [key, prop] of types) {
const raw = vnode.props?.[key];
const value = props[key];
if (
([] as unknown[]).concat(prop.type).includes(Boolean) // Has boolean type
&& prop.required !== true && !('default' in prop) // Optional without default
&& raw === undefined && value === false // Casted to false by vue
) {
/**
* Reset boolean props value to undefined
* @hack `props` is readonly, use `props.__v_raw` to get a mutable `Proxy`
*/
(props as any).__v_raw[key] = undefined;
}
}
},
}); |
This definitely violates principle of least surprise when all (?) other types default to undefined when not specified. But changing this behavior would be a breaking change and is not going to happen in Vue 3. See comment from core dev: vuejs/core#9882 (comment) A couple workarounds:
Either of these seem to elicit the expected behavior. |
I have to do it for this kind of scene:
<template>
<q-input
v-bind="props"
...
/>
</template>
<script lang="ts" setup>
import { QInput, QInputProps } from 'quasar';
interface Props extends QInputProps {
...
}
const props = defineProps<Props>();
</script> The paradox is that before vue 3.3-, I didn’t need to write |
Why vue core function resolvePropValue doesn't check opt.required instead casting every boolean prop? diff --git a/dist/vue.esm-browser.js b/dist/vue.esm-browser.js
index 41e2d441bc75a4d11664c3fb7212eb0355eedb8c..2b3d1567b3415976293603ee73fa57063d54a17f 100644
--- a/dist/vue.esm-browser.js
+++ b/dist/vue.esm-browser.js
@@ -5754,7 +5754,7 @@ function resolvePropValue(options, props, key, value, instance, isAbsent) {
}
}
if (opt[0 /* shouldCast */]) {
- if (isAbsent && !hasDefault) {
+ if (isAbsent && !hasDefault && opt.required) {
value = false;
} else if (opt[1 /* shouldCastTrue */] && (value === "" || value === hyphenate(key))) {
value = true;
` `` |
你都理解错了好吧,官方文档上说的是 attr 是 boolean 的话,如果没传默认就是 false。包含 boolean 类型和只是 boolean 类型是两种完全不同的表述好吧 // 我指定的类型里包含 boolean 类型,又不只是 boolean 类型,这两者表述是有明显差别的好吧,你自己错误的理解了官方文档,却还不听取广大开发者的建议。
type Prop = {
prop1?: boolean | string
prop2: boolean // 这个不传默认为 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 bothmyProp
andmissingProp
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 totrue
(somewhat related to #4538) but I guess this is intended. I'm usingdefault: undefined
for now.The text was updated successfully, but these errors were encountered: