Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion packages/runtime-core/src/componentPublicInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,46 @@ export type CreateComponentPublicInstance<
ComponentOptionsBase<P, B, D, C, M, Mixin, Extends, E, string, Defaults>
>

type ModelProps<T> = Exclude<{
[Prop in keyof T]: T extends { [k in Prop as `onUpdate:${k & string}`]?: any }
? Prop
: never
}[keyof T], undefined>

type RequiredModelProps<T> = Exclude<{
[Prop in keyof T]: T extends { [k in Prop]: any }
? T extends { [k in Prop as `onUpdate:${k & string}`]?: any }
? Prop
: never
: never
}[keyof T], undefined>

type ModelFor<T extends string | number | symbol, V> =
T extends 'modelValue'
? | { [k in T]: V }
| { [k in `v-model:${T & string}`]: V }
| { 'v-model': V }
: | { [k in T]: V }
| { [k in `v-model:${T & string}`]: V }

type NotNever<T> = [T] extends [never] ? {} : T

type Unmap<T, U = UnionToIntersection<Exclude<T, undefined>>> =
NotNever<U extends { mapped: any } ? U['mapped'] : never>

type MakeModelTypes<
T,
R extends keyof T = RequiredModelProps<T>,
O extends keyof T = Exclude<ModelProps<T>, R>
> =
& Unmap<{
[K in R]: { mapped: ModelFor<K, T[K]> }
}[R]>
& Unmap<{
[K in O]: { mapped: Partial<ModelFor<K, T[K]>> }
}[O]>
& Omit<T, R | O>

// public properties exposed on the proxy, which is used as the render context
// in templates (as `this` in the render option)
export type ComponentPublicInstance<
Expand All @@ -181,7 +221,7 @@ export type ComponentPublicInstance<
$: ComponentInternalInstance
$data: D
$props: MakeDefaultsOptional extends true
? Partial<Defaults> & Omit<P & PublicProps, keyof Defaults>
? Partial<Defaults> & MakeModelTypes<Omit<P & PublicProps, keyof Defaults>>
: P & PublicProps
$attrs: Data
$refs: Data
Expand Down
97 changes: 88 additions & 9 deletions test-dts/defineComponent.test-d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -891,15 +891,6 @@ describe('defineComponent', () => {
})

describe('emits', () => {
// Note: for TSX inference, ideally we want to map emits to onXXX props,
// but that requires type-level string constant concatenation as suggested in
// https://github.com/Microsoft/TypeScript/issues/12754

// The workaround for TSX users is instead of using emits, declare onXXX props
// and call them instead. Since `v-on:click` compiles to an `onClick` prop,
// this would also support other users consuming the component in templates
// with `v-on` listeners.

// with object emits
defineComponent({
emits: {
Expand Down Expand Up @@ -1005,6 +996,94 @@ describe('emits', () => {
}
})

// Optional model
{
const Component = defineComponent({
props: {
modelValue: String,
value: Number
},
emits: {
'update:modelValue': (n: string) => typeof n === 'string',
},
setup (props) {
// @ts-expect-error
props['v-model']
}
})
;[
<Component />,
<Component value={ 3 } />,
<Component v-model="3" />,
<Component modelValue="3" />,
<Component v-model:modelValue="3" />,
<Component v-model="3" value={ 3 } />,
<Component modelValue="3" value={ 3 } />,
<Component v-model:modelValue="3" value={ 3 } />,
]
}

// Required model
{
const Component = defineComponent({
props: {
modelValue: {
type: String,
required: true,
},
value: Number
},
emits: {
'update:modelValue': (n: string) => typeof n === 'string',
},
setup (props) {
// @ts-expect-error
props['v-model']
}
})
;[
// @ts-expect-error
<Component />,
// @ts-expect-error
<Component value={ 3 } />,
<Component v-model="3" />,
<Component modelValue="3" />,
<Component v-model:modelValue="3" />,
<Component v-model="3" value={ 3 } />,
<Component modelValue="3" value={ 3 } />,
<Component v-model:modelValue="3" value={ 3 } />,
]
}

// Multiple models
{
const Component = defineComponent({
props: {
modelValue: {
type: String,
required: true,
},
value: {
type: Number,
required: true,
}
},
emits: {
'update:modelValue': (n: string) => typeof n === 'string',
'update:value': (n: number) => typeof n === 'number',
}
})
;[
// @ts-expect-error
<Component />,
// @ts-expect-error
<Component value={ 3 } />,
// @ts-expect-error
<Component modelValue="3" />,
<Component v-model="3" v-model:value={ 3 } />,
]
}

// without emits
defineComponent({
setup(props, { emit }) {
Expand Down