Subscribe on changes!

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

avatar
Sep 1st 2021

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

  • Passing in the function when the component is called.

  • Write directly to the type of the default function instead import it:

<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

  • The default prop function in the <template> correctly returns the function itself.

  • The same issue occurs when the default prop function return a Promise.

  • Here is my npx envinfo --binaries --system:

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
  • the example above was created using yarn create vite with typescript.
avatar
Sep 1st 2021

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

avatar
Sep 1st 2021

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
});
avatar
Sep 1st 2021

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.

avatar
Sep 1st 2021

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.

avatar
Sep 1st 2021

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.

avatar
Sep 1st 2021

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 In the output js code.You can find that the type of fetchData is null rather than Function

avatar
Sep 1st 2021

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.