Subscribe on changes!

Reactivity Transform wraps default function in function

avatar
Nov 16th 2022

Vue version

3.2.45 (also checked with f3e4f03)

Link to minimal reproduction

https://sfc.vuejs.org/#__DEV__eNp9kUtuwzAMRK9CaGWjjQVk6ToueoMeQIs4DpM4sCmBlNJF4LuX/qBtWqAbgaRmBsTj3byFUNwSmtJU0nIXIgjGFKBv6LxzJooztaOOIvKpaRHe2QeBuyOAoQkB+bWEjEqgNByQc9jVa+loVJEja0EuPvVHYJTUR+gI9osVdkCzAZ5gu4dDiqtGHkXZHJt9a/O9o9aTRLivWzxEwajtEU8d4bxuNb91lr84Woy+x6L352wxZ9s8d1TZBUCtS2sXcQh9E7Gu7Fdpnk03BM9xo8biKp6U28zCrR+Kq1zoTDMFO/XOXGIMUlorp3aifZXC89lqVXCi2A1YoAybA/sPQdZgZ55/ZFgd3pA3jHREVrT/ZP6S/smdYvUwoxk/AVe7q7A=

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.

avatar
Nov 16th 2022

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

avatar
Nov 16th 2022

I think the compiler should not add () => into default code.

avatar
Nov 16th 2022

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?

avatar
Nov 16th 2022

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

image

https://vuejs.org/guide/components/props.html#prop-validation

image

// 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

avatar
Nov 16th 2022

I think the result () => (n => n + 2) is right, the default props suggest use a factory function. ~ snip ~ I think the Function 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.

avatar
Nov 16th 2022

I think the result () => (n => n + 2) is right, the default props suggest use a factory function. ~ snip ~ I think the Function 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.

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.

avatar
Nov 16th 2022

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==

avatar
Nov 16th 2022

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>
avatar
Nov 16th 2022

withDefaults can indeed circumvent this problem, but it is confusing why const { mapper = n => n + 2 } will produce this weird behavior

avatar
Nov 16th 2022

withDefaults can indeed circumvent this problem, but it is confusing why const { 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'
    }) 
}
avatar
Nov 16th 2022

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 */ },
})
avatar
Nov 16th 2022

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 */ },
})

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.

avatar
Nov 16th 2022

@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.

avatar
Jan 26th 2023

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.

avatar
Feb 9th 2023

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,
});
Capture d’écran, le 2023-02-09 à 02 57 40

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']
})