withDefaults disables the use of discriminated unions (only in scripts, not in templates)
Vue version
3.3.4
Link to minimal reproduction
Steps to reproduce
Check Comp.vue
- in the template, the discriminated union can be inferred correctly. Not so in the script.
This is probably because withDefaults uses Omit
to combine the required and optional props and because Omit follows the following behavior: https://github.com/microsoft/TypeScript/issues/31501.
I'm not sure why the discriminated union is inferred correctly in the template, though.
defineProps
itself, without defaults, works correctly - check Comp1.vue
.
What is expected?
Discriminated unions should work in scripts as well as in templates.
What is actually happening?
Only works in templates.
System Info
No response
Any additional comments?
No response
Added PR https://github.com/vuejs/core/pull/9336 - my first one, be kind :)
Looks good! Question, how can we use this in combination with defineModel?
So have mode: multiple
on the defineProps type and modelValue: T[]
on defineModel.
interface BaseProps {
label: string
}
interface PropsMulti extends BaseProps {
mode: 'multiple'
modelValue: T[]
}
interface PropsSingle extends BaseProps {
mode: 'single'
modelValue: T
}
type Props = PropsMulti | PropsSingle
const props = withDefaults(defineProps<Omit<Props, 'modelValue'>>(), {
mode: 'single'
})
const modelValue = defineModel<Pick<Props, 'modelValue'>>('modelValue', { required: true })
I don't think this is doable at all?
Good point! I too have been wondering how to connect defineProps and defineModel on a type level with regards to discriminated unions. Let me think about it. Maybe @pikax has an idea too?
@andredewaard defineModel
is a macro to simplify the model
manipulation, you won't be able to discriminate the type in your example, since those are two completely different types. As an exercise I would recommend trying to achieve that in plain Typescript :)
Tried the same thing as @andredewaard , I just used:
const modelValue = defineModel<Props['modelValue']>({ required: true })
instead of:
const modelValue = defineModel<Pick<Props, 'modelValue'>>('modelValue', { required: true })
This compiles, but the property type just includes all possible types of 'modelValue' property (both T and T[]). And I don't think it is possible to solve this, since as @pikax says, these are two separate types (the omit one used in defineProps and lookup one used in defineModel). So TypeScript can't infer the type properly.
My conclusion - don't use defineModel in components where dicriminated union props are required. A little unfortunate, not being able to use the new feature always, but hey... what can you do 😆
@StepanMynarik Thanks, i think i would be using the old way with modelValue and emit
the only problem with this is that we still emit all possible options because they are separate types.
Example:
export type OptionProps = {
value: string | number
label: string
}
interface BaseProps {
options: OptionProps[]
}
interface PropsSingle extends BaseProps {
modelValue: OptionProps['value']
multiple: false
}
interface PropsMulti extends BaseProps {
modelValue: [OptionProps['value']]
multiple: true
}
type Props = PropsMulti | PropsSingle
const props = defineProps<Props>()
const emit = defineEmits<{
(event: 'update:modelValue', value: Props['modelValue']): void
}>()