Reactivity Transform wraps default function in function
Vue version
3.2.45 (also checked with f3e4f03)
Link to minimal reproduction
Steps to reproduce
It can easily be seen in the JS output, but for convenience the console log shows as well that a wrapped function is returned instead of the expected result.
What is expected?
With activated reactivity-transform:
const { mapper = n => n + 2 } = defineProps<{mapper?: (n: number) => number}>()
should result in something equal to:
const mapper = n => n + 2
What is actually happening?
Instead, it results in
const mapper = () => (n => n + 2)
System Info
No response
Any additional comments?
I tried to reproduce it in different ways. It doesn't matter if the function is given directly as a default or is being imported from somewhere else. The example is the most simplified version I found.
When I tried to revise this question, I found that there is a test about this situation in the unit test, it seems that the design is like this(line 82 ~ 102) see
It's a bug I think. The type of func
is () => number
. But in the runtime code, it's () => (() => 1)
.
When I tried to revise this question, I found that there is a test about this situation in the unit test, it seems that the design is like this(line 82 ~ 102) see
It really looks like the test is specifically testing the erroneous behavior. Maybe a copy&paste mistake?
I think the result () => (n => n + 2)
is right, the default props suggest use a factory function.
https://vuejs.org/guide/typescript/composition-api.html#props-default-values
https://vuejs.org/guide/components/props.html#prop-validation
// Object or array defaults must be returned from // a factory function. The function receives the raw // props received by the component as the argument.
I think the Function
may also need use a factory function
I think the result
() => (n => n + 2)
is right, the default props suggest use a factory function. ~ snip ~ I think theFunction
may also need use a factory function
I don't think this makes sense. There is no reason to transport functions inside functions. The reason for using factory functions is to avoid pointing at the same address space for supposedly different objects and accidentally create a shared state between components. This wouldn't happen with a function.
I think the result
() => (n => n + 2)
is right, the default props suggest use a factory function. ~ snip ~ I think theFunction
may also need use a factory functionI don't think this makes sense. There is no reason to transport functions inside functions. The reason for using factory functions is to avoid pointing at the same address space for supposedly different objects and accidentally create a shared state between components. This wouldn't happen with a function.
As you say, I think the function
also is a Function object in JavaScript, the function also can be shared state between components and be modified by component.
I think the
function
also is a Function object in JavaScript, the function also can be shared state between components and be modified by component.
Sure, technically that might be right. But first the behaviour is then different when not using the reactivity-transform, second someone wouldn't expect anything to be automatically wrapped into a function that is then not automatically unwrapped.
Also: it is not happening with objects, which rules out this as a reasoning for it to happen with functions. https://sfc.vuejs.org/#__DEV__eNp9kd1ugzAMhV/Fys1Aa4nUS0aZ9gZ7gFwUqKFU4ER20l1UffeFn3XrJk2KIic5/nJsX9Wbc9kloMpVIQ33zoOgDw6Girq9UV6MKg315JHbqkF4Z+sEroYAxso55NccEsqBwlgjp7Av13BS2Pocn69A1Yg5iOeeOrgZigsMaQ1ysmE4AqOEwUNPcFigsAeaUfAMuwPUwa8aeRQl84fJtzY9GGosif9p8YG2WY1FX1MEq7mn1tqn6WKyFxOO2PaEc7XFvJdJ+mJoodsBs8F2yYJPdmlqqNBL/8pYWTx5HN1QeSwLfQ/VRvWjs+y3MTE7i6XY9tmFWR9it+++jIpzmc5Gnbx3kmstbTMN6yyZ5U7HKONAvh8xQxm3NdsPQY5go+YqV4aOlxfkLSMdkeNk/mH+kv7hfnVI3T4B/zC/kA==
I think the
function
also is a Function object in JavaScript, the function also can be shared state between components and be modified by component.Sure, technically that might be right. But first the behaviour is then different when not using the reactivity-transform, second someone wouldn't expect anything to be automatically wrapped into a function that is then not automatically unwrapped.
If do that (don't use a factory function), there will be has hidden risks for other people who use this syntax. I think you may use like this:
<script setup lang="ts">
interface Props {
mapper?: (n: number) => number
}
const props = withDefaults(defineProps<Props>(), {
mapper: n => n + 2
})
console.log(props.mapper(2))
</script>
<template></template>
withDefaults
can indeed circumvent this problem, but it is confusing why const { mapper = n => n + 2 }
will produce this weird behavior
withDefaults
can indeed circumvent this problem, but it is confusing whyconst { mapper = n => n + 2 }
will produce this weird behavior
It's not confusing behaviour, in @nkoehring lastest demo the Object also use a factory function.
props: {
mapper: { type: Function, required: false, default: () => (n => n + 2) },
obj: { type: Object, required: false, default: () => ({
name: 'foo'
})
}
As the docs mentioned, for the object type, it's good to provide the function to generate the default value ({ type: Object, default: () => ({ foo: 'foo' }) }
). But this feature is for Array
and Object
only, not for functions.
For a function default value, it's impossible to figure out if it's exactly the value passed to the prop, or if it's the generator to produce the prop value.
For example: if the expected default value is () => 'hello'
:
{ type: Function, default: () => 'hello' }
v.s { type: Function, default: () => () => 'hello' }
We're unable to know the returning value before executing it.
So that's why you cannot put the generator of function to a default value.
Back to the topic, the compiler cannot know what the type of props is (runtime definition), so it is also unlikely to add or not add () =>
automatically.
As a result, I suggest we should remove this feature of auto-adding () =>
, and let the users consider it, just like withDefaults
or defineProps
with runtime object.
defineProps({
foo: { type: Object, default() { return { foo: 'foo' } /* it's a generator */ } },
bar: { type: String, default: 'foo' /* it's a plain value */ },
baz: { type: Function, default: () => 'foo' /* it's a plain value */ },
})
As the docs mentioned, for the object type, it's good to provide the function to generate the default value (
{ type: Object, default: () => ({ foo: 'foo' }) }
). But this feature is forArray
andObject
only, not for functions.For a function default value, it's impossible to figure out if it's exactly the value passed to the prop, or if it's the generator to produce the prop value.
For example: if the expected default value is
() => 'hello'
:{ type: Function, default: () => 'hello' }
v.s{ type: Function, default: () => () => 'hello' }
We're unable to know the returning value before executing it.
So that's why you cannot put the generator of function to a default value.
Back to the topic, the compiler cannot know what the type of props is (runtime definition), so it is also unlikely to add or not add
() =>
automatically.As a result, I suggest we should remove this feature of auto-adding
() =>
, and let the users consider it, just likewithDefaults
ordefineProps
with runtime object.defineProps({ foo: { type: Object, default() { return { foo: 'foo' } /* it's a generator */ } }, bar: { type: String, default: 'foo' /* it's a plain value */ }, })
If will remove this feature of auto-adding () =>
, this may be a broken change, must in the Minor version such as 3.3.x or 3.4.x and so on.
@liulinboyi It's the scope of Reactivity Transform, and Reactivity Transform is under experimental and unstable. Maybe we can fix it in Vue 3.3, since Evan said it will be stabilized in the version.
Unfortunately, Reactivity Transform has been dropped officially now, so I closed this issue/PR. If you want to keep using it, please consider the community version Vue Macros.
Is this related ?
export interface Props {
expanded: boolean;
duration?: number;
hwAcceleration?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
expanded: false,
duration: () => inject<PluginOptions>(PLUGIN_KEY)?.duration || 300,
hwAcceleration: () => inject<PluginOptions>(PLUGIN_KEY)?.hwAcceleration || false,
});
Gives me:
Type '() => boolean' is not assignable to type 'boolean | ((props: Readonly<Omit<Props, "expanded" | "hwAcceleration"> & { expanded: boolean; hwAcceleration: boolean; }>) => false) | ((props: Readonly<Omit<Props, "expanded" | "hwAcceleration"> & { ...; }>) => true) | undefined'. Type '() => boolean' is not assignable to type '(props: Readonly<Omit<Props, "expanded" | "hwAcceleration"> & { expanded: boolean; hwAcceleration: boolean; }>) => false'.
hwAcceleration
is a boolean, but has a factory function to resolve it's default value.
duration
has the same, but doesn't make an error.
Changing my interface like so works:
export interface Props {
expanded: boolean;
duration?: () => number;
hwAcceleration?: () => boolean;
}
But it seems wrong and shouldn't be required from what I'm reading in the Vue documentation as you can see in the exemple here.
The type of labels
is string[]
, not () => string[]
.
export interface Props {
msg?: string
labels?: string[]
}
const props = withDefaults(defineProps<Props>(), {
msg: 'hello',
labels: () => ['one', 'two']
})