core: Problem typing optional props with exactOptionalPropertyTypes enabled in tsconfig

Vue version

3.2.37

Link to minimal reproduction

https://sfc.vuejs.org/#eNqdUzuP1DAQ/itWqpx0iREnhBSyuaUAiQaoqNxkk9nFt/FDfgShkP/OOE72QnK6gs6eGX+PmfGQfNQ67z0kRVI6ELqrHVRMElJ+kdq7r16cwJA+E6qF7sCSvu48sITQWKSrYSBTjDw+EpZ42cIZ0+NYUh1LTt45Jcmx6XhzXRDIgbx7w5Lq27WksQCLS7pSgFfbGK4dseC8Jl0tL/jaWXzFJBdaGUcGYuBMRnI2SiA7+mDJLbk2MBfkdBUMtkM5k42S1s02DgGylPHZHzIZ4hLaKr0d7z4ErVEdaknuk8iYiVrnT1ZJbOYQvLM5gZoLMkVCbKIt8PDTOW0LSr3U10veKEGPmKPGS8cFZK0Sx4f8bf7wnrbcunU8Byuyk1G/LNp4Qvj7FTjFYA8mM4CCDZhXyTa1/xBucjvSwDkyOWIDNm3dbxMPBas9arxBXPdjXif3WwNGY9vn9fqfdUBfyAPtbieWIWujtMUhx1F+D7dymswkbJJTkP300WWVhrkvOCC4u8F8wssMk+Jz/Aa6RdnFMyZOKO7XS+B3BekV33Ose4Rci7d0IrqAS/HhHm1ZNIN9MjIazp+VID5ObdoX7GS6FWWd4fLygroFdvklujYWPneqnjGC8JAPjUlf60AsDArG7Uca/wK66Je8

Steps to reproduce

See reproduction link for full example.

I declare a property which allows undefined:

const props = defineProps<{
  modelValue: number | undefined
}>();

and actually pass undefined:

<template>
  <TheComponent v-model="value" />
</template>

<script setup lang="ts">
const value = ref<number | undefined>(undefined);
</script>

What is expected?

Vue doesn’t emit runtime errors for explicitly allowed and used undefined properties.

What is actually happening?

I get the following Vue runtime error in dev mode

[Vue warn]: Invalid prop: type check failed for prop "modelValue". Expected Number | Null, got Undefined  

This is because the SFC compiler emits the following property declaration. Note how null was used instead of undefined:

props: {
  modelValue: { type: [Number, null], required: true }
},

If I modify the generated code and runtime like below (see <<< and >>>), to allow undefined, it seems to work correctly. But I don’t understand if there are other reasons for not allowing undefined to be a required property value:

// Component
props: {
  modelValue: { type: [Number, undefined], required: true }
},

// Vue Runtime
function getType(ctor) {
  const match = ctor && ctor.toString().match(/^\s*function (\w+)/);
  // <<<<<<<< added `undefined` support >>>>>>>>>>
  return match ? match[1] : ctor === null ? "null" : ctor === undefined ? "undefined" : "";
}

function assertType(value, type) {
  let valid;
  const expectedType = getType(type);
  if (isSimpleType(expectedType)) {
    const t = typeof value;
    valid = t === expectedType.toLowerCase();
    if (!valid && t === "object") {
      valid = value instanceof type;
    }
  } else if (expectedType === "Object") {
    valid = isObject(value);
  } else if (expectedType === "Array") {
    valid = isArray$1(value);
  } else if (expectedType === "null") {
    valid = value === null;
  // <<<<<<<< added `undefined` support >>>>>>>>>>
  } else if (expectedType === "undefined") {
    valid = value === undefined;
  } else {
    valid = value instanceof type;
  }
  return {
    valid,
    expectedType
  };
}

System Info

System:
  OS: Windows
Binaries:
  Node: 18.7.0
  npm: 8.15.0
npmPackages:
  vue: ^3.2.37 => 3.2.37

Any additional comments?

I’m using undefined when a value is not set by the user, for example empty number <input> or unselected <select>, as I understood to be current recommended practices:

Use undefined. Do not use null. https://github.com/Microsoft/TypeScript/wiki/Coding-guidelines#null-and-undefined

About this issue

  • Original URL
  • State: open
  • Created 2 years ago
  • Reactions: 5
  • Comments: 15 (7 by maintainers)

Commits related to this issue

Most upvoted comments

Any progress on this issue?

@pikax So there’s two layers to this:

  1. Runtime/codegen: There’s the problem of code generation: an optional?: prop generates a proper runtime prop definition, while a prop with x | undefined doesn’t. The PR that was already submitted seems to fix this.
  2. Types: the types that vue-tsc/Volar generates from the defineProps generic argument are not practical when exactOptionalPropertyTypes is being used. This has to be solved in Volar.

Would you agree?

Okay, so I can kinda reproduce this, but a full repro from your side in a github repo would be apprechiated to make sure we are looking at the same things.

I have:

  • exactOptionalPropertyTypes: true in my tsconfig
  • Typescript 4.6 in my project i’m testing with. (yes, not latest)
  • latest Volar / vue-tsc (0.40.1)

This is HelloWorld.vue …:

const props = defineProps<{
  msg?: string | undefined;
}>();

This is what I have in the parent’s state:

import { ref, Ref } from 'vue'

const nameRef: Ref<string | undefined> = ref("Tom");
let name: string | undefined = "Tom";

Both, when used for msg, will throw the error.

image

I think it is because the type generated for the props object that will be passed to the child is:

{
  msg: string | undefined
}

while the type from the child component is:

{
  msg?: string | undefined
}

So we could get rid of the question mark, but then we can’t leave the prop completely absent in the parent.

Not sure if this needs to be solved in Vue core, our JSX types or vue-tsc?

/cc @pikax @johnsoncodehk What do you think?