vuejs / core

🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
https://vuejs.org/
MIT License
47.4k stars 8.29k forks source link

withDefaults() infers wrong type when using a default value factory function #4480

Open HoPGoldy opened 3 years ago

HoPGoldy commented 3 years ago

Version

3.2.6

Reproduction link

github.com - vue3-issue-default-function-prop

Steps to reproduce

I have the following functions and the same type definitions in service.ts, This function simply returns a string:

export const defaultFunc = function(): string {
    return 'result'
}

export type DefaultFunc = () => string

Then import them in a setup script and set props, defaults using defineProps and withDefault:

<script setup lang="ts">
import { defaultFunc } from './service';
import type { DefaultFunc } from './service';

interface Props {
  fetchData?: DefaultFunc
}

// set default function prop
const props = withDefaults(defineProps<Props>(), {
  fetchData: defaultFunc
});

// print default prop function value
console.log('======> default prop fetchData:', props.fetchData);
</script>

call this component and does not pass the prop fetchData, then check the console output of the web page.

What is expected?

Then console print the function itself:

======> default prop fetchData: ƒ () {
  return "result";
}

What is actually happening?

the console prints the return value of the default function, not the function itself.

======> default prop fetchData: result

How to resolve the issue

<script setup lang="ts">
import { defaultFunc } from './service';

interface Props {
  fetchData?: () => string // <=============== look here
}

const props = withDefaults(defineProps<Props>(), {
  fetchData: defaultFunc
});

console.log('======> default prop fetchData:', props.fetchData);
</script>

Then console will print the function itself correctly:

======> default prop fetchData: ƒ () {
  return "result";
}

Other ways to trigger the issue

Use typeof to get the function type and use it in the props interface:

<script setup lang="ts">
import { defaultFunc } from './service';

interface Props {
  fetchData?: typeof defaultFunc // <=============== look here
}

const props = withDefaults(defineProps<Props>(), {
  fetchData: defaultFunc
});

console.log('======> default prop fetchData:', props.fetchData);
</script>

More information

System:
    OS: Windows 10 10.0.18363
    CPU: (8) x64 Intel(R) Core(TM) i5-1035G1 CPU @ 1.00GHz
    Memory: 6.18 GB / 15.77 GB
Binaries:
    Node: 12.16.1 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.22.10 - C:\Program Files\nodejs\yarn.CMD
    npm: 6.13.4 - C:\Program Files\nodejs\npm.CMD
LinusBorg commented 3 years ago

This is expected and not related to withDefault() specifically.

Functions as default value will be executed to get the actual default value. This is useful/needed when the default value should be an fresh object for each component instance, for example.

The solution is to wrap the function in another function:

const props = withDefaults(defineProps<Props>(), {
  fetchData: () => defaultFunc
});

Though I can't test it right now and verify that types like that solution as well ...

LinusBorg commented 3 years ago

Ok could test it. Types don't like it.

As a temporary workaround you can typecast it like so:

const props = withDefaults(defineProps<Props>(), {
  fetchData: (() => defaultFunc) as unknown as DefaultFunc
});
HoPGoldy commented 3 years ago

Thanks for your quick answer!

In general, this truly useful, when I need a string[] prop, ts will prompt me to set a function for default as a factory.

interface Props {
  tags: string[]
}

const props = withDefaults(defineProps<Props>(), {
  tags: () => []
});

Along this line of thinking, I think withDefaults should prompt me to provide a function as the "function factory". But as you can see, withDefaults wants me to provide a function with exactly same type.

HoPGoldy commented 3 years ago

And strangely, when I used the import DefaultFunc type in props interafce, default function was executed.

import type { DefaultFunc } from './service';

interface Props {
  fetchData?: DefaultFunc
}

// props.fetchData print "result" on console

But when I replaced DefaultFunc with type () => string, the function was not executed.


interface Props {
  fetchData?: () => string
}

// props.fetchData print f() { return "result" } on console

I can't find difference between them.

Bigfish8 commented 3 years ago

Functions as default value will be executed to get the actual default value.

In fact,when prop type is Function,vue will not call the default value,see code.

interface Props {
  fetchData?: () => string
}

// set default function prop
const props = withDefaults(defineProps<Props>(), {
  fetchData: () => '1'
});

The type of fectchData is Function.So the () => '1' will not be called.

Bigfish8 commented 3 years ago

I can't find difference between them.


import type { DefaultFunc } from './service';

interface Props { fetchData?: DefaultFunc }



This is because In the condition, sfc-compiler can not generate the right type,see [code](https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHNjcmlwdCBzZXR1cCBsYW5nPVwidHNcIj5cbmltcG9ydCB7IGRlZmF1bHRGdW5jIH0gZnJvbSAnLi9zZXJ2aWNlJztcbmltcG9ydCB0eXBlIHsgRGVmYXVsdEZ1bmMgfSBmcm9tICcuL3NlcnZpY2UnO1xuXG5pbnRlcmZhY2UgUHJvcHMge1xuICBmZXRjaERhdGE/OiBEZWZhdWx0RnVuY1xufVxuXG5jb25zdCBwcm9wcyA9IHdpdGhEZWZhdWx0cyhkZWZpbmVQcm9wczxQcm9wcz4oKSwge1xuICBmZXRjaERhdGE6IGRlZmF1bHRGdW5jXG59KTtcblxuY29uc29sZS5sb2coJz09PT09PT4gZGVmYXVsdCBwcm9wIGZldGNoRGF0YTonLCBwcm9wcy5mZXRjaERhdGEpO1xuPC9zY3JpcHQ+Iiwic2VydmljZS50cyI6ImV4cG9ydCBjb25zdCBkZWZhdWx0RnVuYyA9IGZ1bmN0aW9uKCk6IHN0cmluZyB7XG4gICAgcmV0dXJuICdyZXN1bHQnXG59XG5cbmV4cG9ydCB0eXBlIERlZmF1bHRGdW5jID0gKCkgPT4gc3RyaW5nIn0=)
In the output js code.You can find that the type of `fetchData` is `null` rather than `Function`
yyx990803 commented 3 years ago

compiler-sfc infers the runtime prop type from your TS Props interface. When you use an imported type, compiler-sfc currently doesn't crawl external files and thus does not have the type information from the external. So it can only generate a loose runtime type - and because the runtime type is not Function, the default value will be treated as a factory instead of an actual value.

This is currently a known limitation, the workaround is to prefer writing props types in the same file instead of importing it.