-
-
Notifications
You must be signed in to change notification settings - Fork 9.1k
Description
Vue version
3.5.27
Link to minimal reproduction
Steps to reproduce
Note
I'm a little confused with the version that introduced the regression.
In Playground (see repro link) the behavior is broken since 3.5.15.
In my own project, it looks like 3.5.18 is the last working version... not sure why, maybe it depends on the vite-plugin version as well, or something being more complex in my code than in the repro.
The situation is this: we have a component property that has an array default value, provided like this:
withDefaults(
defineProps<{ data }>(),
{ data: () => [] }
)The repro link uses Vue 3.5.14 and it shows "empty array" initially.
If you switch to Vue 3.5.15+ then it shows "ERROR" because data is not an array, but the default function () => [].
The trick here is that the typing of data is somewhat hidden in a global type declaration somewhere else. This is key for the bug to reproduce, see below for full analysis.
What is expected?
data should be an empty array, provided by the default () => [].
This is what happens until 3.5.14.
Notice that the generated props is described as type: null. This is important.
What is actually happening?
data is the default function itself () => [] instead of an array.
Starting with 3.5.15 it looks like SFC compiler is smarter and infers a prop type based on Typescript types.
If you declare
defineProps<{ data: number[] | Promise<number[]> }>()Then the prop has { type: Promise | Array } and it works.
In the repro, I'm using a globally declared MaybePromise<T> = Promise<T> | T.
As the compiler doesn't do global program analysis, it cannot know that.
At this step the prop would not have a type, as compiler doesn't know MaybePromise<>.
But the repro has a union with another, known type: MaybePromise<number[]> | Fetch<number>.
The compiler knows the type of Fetch<> (a function), so it generates the following props, which is incorrect:
{ type: Function }This generation has 2 problems:
- Because it's not the full type, it generates dev warnings when binding an array or a Promise.
- It seems to influence the default value assignment, and
data === () => []when unset. I assume Vue assigns the function "as-is" when the property type isFunction.
System Info
Vue SFC PlaygroundAny additional comments?
Because of the dev warnings (point 1), it seems to me that the compiler MUST be sure that it could analyze the whole Typescript type.
If the type is a union and some parts are unknown to compiler, then it should consider that any type can be assigned.
This is consistent with previous Vue releases, and it's conservative for warnings (false-negative but no false-positive).
It also fixes the regression regarding point 2.
I am not a fan of factories being assigned "as-is" to props of type Function.
This creates inconsistencies and situations that can't be resolved.
Because of backward compatibility, I guess it's too late to change it (or have a compatibility flag in compiler options?).
At first glance it's tempting to say if default is a function and the props is a function, then let's treat it as a value.
But functions can be mutated, like objects. The docs calls out that default values are shared amongst components, so mutable values should be provided by factories.
So it might be an edge case, but if you want a function that you can mutate as your default value... well you can't.
// Doesn't work
{ mutableFunc: () => ( () => "whatever" ) }For the same reason if you'd like to return a different default function based on some global application state... it's not possible either.
It also creates inconsistent situations where the default value depends on the declared property type.
If you happily start with a prop fn: () => number with default { fn: () => 0 } it works.
If later you decide to accept plain numbers as well and change the declaration to fn: number | (() => number), then you should be careful... because there is "action at a distance" here: the default is now 0, provided by a factory.
Unless you change it to () => () => 0, so that the default is still a function, but that is sooo inconsistent with my previous example where this syntax did not work
If it were possible, I think it would be more reasonable to say default value of type functions are always treated as factories, regardless of prop type.