mirror of https://github.com/vuejs/core.git
feat(types): `defineComponent()` with generics support (#7963)
BREAKING CHANGE: The type of `defineComponent()` when passing in a function has changed. This overload signature is rarely used in practice and the breakage will be minimal, so repurposing it to something more useful should be worth it. close #3102
This commit is contained in:
parent
9a8073d0ae
commit
d77557c403
|
@ -351,7 +351,7 @@ describe('type inference w/ optional props declaration', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('type inference w/ direct setup function', () => {
|
describe('type inference w/ direct setup function', () => {
|
||||||
const MyComponent = defineComponent((_props: { msg: string }) => {})
|
const MyComponent = defineComponent((_props: { msg: string }) => () => {})
|
||||||
expectType<JSX.Element>(<MyComponent msg="foo" />)
|
expectType<JSX.Element>(<MyComponent msg="foo" />)
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
;<MyComponent />
|
;<MyComponent />
|
||||||
|
@ -1250,10 +1250,130 @@ describe('prop starting with `on*` is broken', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('function syntax w/ generics', () => {
|
||||||
|
const Comp = defineComponent(
|
||||||
|
// TODO: babel plugin to auto infer runtime props options from type
|
||||||
|
// similar to defineProps<{...}>()
|
||||||
|
<T extends string | number>(props: { msg: T; list: T[] }) => {
|
||||||
|
// use Composition API here like in <script setup>
|
||||||
|
const count = ref(0)
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
// return a render function (both JSX and h() works)
|
||||||
|
<div>
|
||||||
|
{props.msg} {count.value}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expectType<JSX.Element>(<Comp msg="fse" list={['foo']} />)
|
||||||
|
expectType<JSX.Element>(<Comp msg={123} list={[123]} />)
|
||||||
|
|
||||||
|
expectType<JSX.Element>(
|
||||||
|
// @ts-expect-error missing prop
|
||||||
|
<Comp msg={123} />
|
||||||
|
)
|
||||||
|
|
||||||
|
expectType<JSX.Element>(
|
||||||
|
// @ts-expect-error generics don't match
|
||||||
|
<Comp msg="fse" list={[123]} />
|
||||||
|
)
|
||||||
|
expectType<JSX.Element>(
|
||||||
|
// @ts-expect-error generics don't match
|
||||||
|
<Comp msg={123} list={['123']} />
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('function syntax w/ emits', () => {
|
||||||
|
const Foo = defineComponent(
|
||||||
|
(props: { msg: string }, ctx) => {
|
||||||
|
ctx.emit('foo')
|
||||||
|
// @ts-expect-error
|
||||||
|
ctx.emit('bar')
|
||||||
|
return () => {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emits: ['foo']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expectType<JSX.Element>(<Foo msg="hi" onFoo={() => {}} />)
|
||||||
|
// @ts-expect-error
|
||||||
|
expectType<JSX.Element>(<Foo msg="hi" onBar={() => {}} />)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('function syntax w/ runtime props', () => {
|
||||||
|
// with runtime props, the runtime props must match
|
||||||
|
// manual type declaration
|
||||||
|
defineComponent(
|
||||||
|
(_props: { msg: string }) => {
|
||||||
|
return () => {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
props: ['msg']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
defineComponent(
|
||||||
|
<T extends string>(_props: { msg: T }) => {
|
||||||
|
return () => {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
props: ['msg']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
defineComponent(
|
||||||
|
<T extends string>(_props: { msg: T }) => {
|
||||||
|
return () => {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
props: {
|
||||||
|
msg: String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// @ts-expect-error string prop names don't match
|
||||||
|
defineComponent(
|
||||||
|
(_props: { msg: string }) => {
|
||||||
|
return () => {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
props: ['bar']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// @ts-expect-error prop type mismatch
|
||||||
|
defineComponent(
|
||||||
|
(_props: { msg: string }) => {
|
||||||
|
return () => {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
props: {
|
||||||
|
msg: Number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// @ts-expect-error prop keys don't match
|
||||||
|
defineComponent(
|
||||||
|
(_props: { msg: string }, ctx) => {
|
||||||
|
return () => {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
props: {
|
||||||
|
msg: String,
|
||||||
|
bar: String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
// check if defineComponent can be exported
|
// check if defineComponent can be exported
|
||||||
export default {
|
export default {
|
||||||
// function components
|
// function components
|
||||||
a: defineComponent(_ => h('div')),
|
a: defineComponent(_ => () => h('div')),
|
||||||
// no props
|
// no props
|
||||||
b: defineComponent({
|
b: defineComponent({
|
||||||
data() {
|
data() {
|
||||||
|
|
|
@ -157,7 +157,7 @@ describe('h support for generic component type', () => {
|
||||||
describe('describeComponent extends Component', () => {
|
describe('describeComponent extends Component', () => {
|
||||||
// functional
|
// functional
|
||||||
expectAssignable<Component>(
|
expectAssignable<Component>(
|
||||||
defineComponent((_props: { foo?: string; bar: number }) => {})
|
defineComponent((_props: { foo?: string; bar: number }) => () => {})
|
||||||
)
|
)
|
||||||
|
|
||||||
// typed props
|
// typed props
|
||||||
|
|
|
@ -122,7 +122,7 @@ describe('api: options', () => {
|
||||||
expect(serializeInner(root)).toBe(`<div>4</div>`)
|
expect(serializeInner(root)).toBe(`<div>4</div>`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('component’s own methods have higher priority than global properties', async () => {
|
test("component's own methods have higher priority than global properties", async () => {
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
methods: {
|
methods: {
|
||||||
foo() {
|
foo() {
|
||||||
|
@ -667,7 +667,7 @@ describe('api: options', () => {
|
||||||
|
|
||||||
test('mixins', () => {
|
test('mixins', () => {
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
const mixinA = {
|
const mixinA = defineComponent({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
a: 1
|
a: 1
|
||||||
|
@ -682,8 +682,8 @@ describe('api: options', () => {
|
||||||
mounted() {
|
mounted() {
|
||||||
calls.push('mixinA mounted')
|
calls.push('mixinA mounted')
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
const mixinB = {
|
const mixinB = defineComponent({
|
||||||
props: {
|
props: {
|
||||||
bP: {
|
bP: {
|
||||||
type: String
|
type: String
|
||||||
|
@ -705,7 +705,7 @@ describe('api: options', () => {
|
||||||
mounted() {
|
mounted() {
|
||||||
calls.push('mixinB mounted')
|
calls.push('mixinB mounted')
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
const mixinC = defineComponent({
|
const mixinC = defineComponent({
|
||||||
props: ['cP1', 'cP2'],
|
props: ['cP1', 'cP2'],
|
||||||
data() {
|
data() {
|
||||||
|
@ -727,7 +727,7 @@ describe('api: options', () => {
|
||||||
props: {
|
props: {
|
||||||
aaa: String
|
aaa: String
|
||||||
},
|
},
|
||||||
mixins: [defineComponent(mixinA), defineComponent(mixinB), mixinC],
|
mixins: [mixinA, mixinB, mixinC],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
c: 4,
|
c: 4,
|
||||||
|
@ -817,6 +817,22 @@ describe('api: options', () => {
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('unlikely mixin usage', () => {
|
||||||
|
const MixinA = {
|
||||||
|
data() {}
|
||||||
|
}
|
||||||
|
const MixinB = {
|
||||||
|
data() {}
|
||||||
|
}
|
||||||
|
defineComponent({
|
||||||
|
// @ts-expect-error edge case after #7963, unlikely to happen in practice
|
||||||
|
// since the user will want to type the mixins themselves.
|
||||||
|
mixins: [defineComponent(MixinA), defineComponent(MixinB)],
|
||||||
|
// @ts-expect-error
|
||||||
|
data() {}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test('chained extends in mixins', () => {
|
test('chained extends in mixins', () => {
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
|
|
||||||
|
@ -863,7 +879,7 @@ describe('api: options', () => {
|
||||||
|
|
||||||
test('extends', () => {
|
test('extends', () => {
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
const Base = {
|
const Base = defineComponent({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
a: 1,
|
a: 1,
|
||||||
|
@ -878,9 +894,9 @@ describe('api: options', () => {
|
||||||
expect(this.b).toBe(2)
|
expect(this.b).toBe(2)
|
||||||
calls.push('base')
|
calls.push('base')
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
const Comp = defineComponent({
|
const Comp = defineComponent({
|
||||||
extends: defineComponent(Base),
|
extends: Base,
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
b: 2
|
b: 2
|
||||||
|
@ -900,7 +916,7 @@ describe('api: options', () => {
|
||||||
|
|
||||||
test('extends with mixins', () => {
|
test('extends with mixins', () => {
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
const Base = {
|
const Base = defineComponent({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
a: 1,
|
a: 1,
|
||||||
|
@ -916,8 +932,8 @@ describe('api: options', () => {
|
||||||
expect(this.c).toBe(2)
|
expect(this.c).toBe(2)
|
||||||
calls.push('base')
|
calls.push('base')
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
const Mixin = {
|
const Mixin = defineComponent({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
b: true,
|
b: true,
|
||||||
|
@ -930,10 +946,10 @@ describe('api: options', () => {
|
||||||
expect(this.c).toBe(2)
|
expect(this.c).toBe(2)
|
||||||
calls.push('mixin')
|
calls.push('mixin')
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
const Comp = defineComponent({
|
const Comp = defineComponent({
|
||||||
extends: defineComponent(Base),
|
extends: Base,
|
||||||
mixins: [defineComponent(Mixin)],
|
mixins: [Mixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
c: 2
|
c: 2
|
||||||
|
|
|
@ -7,7 +7,8 @@ import {
|
||||||
ComponentOptionsMixin,
|
ComponentOptionsMixin,
|
||||||
RenderFunction,
|
RenderFunction,
|
||||||
ComponentOptionsBase,
|
ComponentOptionsBase,
|
||||||
ComponentInjectOptions
|
ComponentInjectOptions,
|
||||||
|
ComponentOptions
|
||||||
} from './componentOptions'
|
} from './componentOptions'
|
||||||
import {
|
import {
|
||||||
SetupContext,
|
SetupContext,
|
||||||
|
@ -17,10 +18,11 @@ import {
|
||||||
import {
|
import {
|
||||||
ExtractPropTypes,
|
ExtractPropTypes,
|
||||||
ComponentPropsOptions,
|
ComponentPropsOptions,
|
||||||
ExtractDefaultPropTypes
|
ExtractDefaultPropTypes,
|
||||||
|
ComponentObjectPropsOptions
|
||||||
} from './componentProps'
|
} from './componentProps'
|
||||||
import { EmitsOptions, EmitsToProps } from './componentEmits'
|
import { EmitsOptions, EmitsToProps } from './componentEmits'
|
||||||
import { isFunction } from '@vue/shared'
|
import { extend, isFunction } from '@vue/shared'
|
||||||
import { VNodeProps } from './vnode'
|
import { VNodeProps } from './vnode'
|
||||||
import {
|
import {
|
||||||
CreateComponentPublicInstance,
|
CreateComponentPublicInstance,
|
||||||
|
@ -86,12 +88,34 @@ export type DefineComponent<
|
||||||
|
|
||||||
// overload 1: direct setup function
|
// overload 1: direct setup function
|
||||||
// (uses user defined props interface)
|
// (uses user defined props interface)
|
||||||
export function defineComponent<Props, RawBindings = object>(
|
export function defineComponent<
|
||||||
|
Props extends Record<string, any>,
|
||||||
|
E extends EmitsOptions = {},
|
||||||
|
EE extends string = string
|
||||||
|
>(
|
||||||
setup: (
|
setup: (
|
||||||
props: Readonly<Props>,
|
props: Props,
|
||||||
ctx: SetupContext
|
ctx: SetupContext<E>
|
||||||
) => RawBindings | RenderFunction
|
) => RenderFunction | Promise<RenderFunction>,
|
||||||
): DefineComponent<Props, RawBindings>
|
options?: Pick<ComponentOptions, 'name' | 'inheritAttrs'> & {
|
||||||
|
props?: (keyof Props)[]
|
||||||
|
emits?: E | EE[]
|
||||||
|
}
|
||||||
|
): (props: Props & EmitsToProps<E>) => any
|
||||||
|
export function defineComponent<
|
||||||
|
Props extends Record<string, any>,
|
||||||
|
E extends EmitsOptions = {},
|
||||||
|
EE extends string = string
|
||||||
|
>(
|
||||||
|
setup: (
|
||||||
|
props: Props,
|
||||||
|
ctx: SetupContext<E>
|
||||||
|
) => RenderFunction | Promise<RenderFunction>,
|
||||||
|
options?: Pick<ComponentOptions, 'name' | 'inheritAttrs'> & {
|
||||||
|
props?: ComponentObjectPropsOptions<Props>
|
||||||
|
emits?: E | EE[]
|
||||||
|
}
|
||||||
|
): (props: Props & EmitsToProps<E>) => any
|
||||||
|
|
||||||
// overload 2: object format with no props
|
// overload 2: object format with no props
|
||||||
// (uses user defined props interface)
|
// (uses user defined props interface)
|
||||||
|
@ -198,6 +222,11 @@ export function defineComponent<
|
||||||
): DefineComponent<PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE>
|
): DefineComponent<PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE>
|
||||||
|
|
||||||
// implementation, close to no-op
|
// implementation, close to no-op
|
||||||
export function defineComponent(options: unknown) {
|
export function defineComponent(
|
||||||
return isFunction(options) ? { setup: options, name: options.name } : options
|
options: unknown,
|
||||||
|
extraOptions?: ComponentOptions
|
||||||
|
) {
|
||||||
|
return isFunction(options)
|
||||||
|
? extend({}, extraOptions, { setup: options, name: options.name })
|
||||||
|
: options
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue