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', () => {
|
||||
const MyComponent = defineComponent((_props: { msg: string }) => {})
|
||||
const MyComponent = defineComponent((_props: { msg: string }) => () => {})
|
||||
expectType<JSX.Element>(<MyComponent msg="foo" />)
|
||||
// @ts-expect-error
|
||||
;<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
|
||||
export default {
|
||||
// function components
|
||||
a: defineComponent(_ => h('div')),
|
||||
a: defineComponent(_ => () => h('div')),
|
||||
// no props
|
||||
b: defineComponent({
|
||||
data() {
|
||||
|
|
|
@ -157,7 +157,7 @@ describe('h support for generic component type', () => {
|
|||
describe('describeComponent extends Component', () => {
|
||||
// functional
|
||||
expectAssignable<Component>(
|
||||
defineComponent((_props: { foo?: string; bar: number }) => {})
|
||||
defineComponent((_props: { foo?: string; bar: number }) => () => {})
|
||||
)
|
||||
|
||||
// typed props
|
||||
|
|
|
@ -122,7 +122,7 @@ describe('api: options', () => {
|
|||
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({
|
||||
methods: {
|
||||
foo() {
|
||||
|
@ -667,7 +667,7 @@ describe('api: options', () => {
|
|||
|
||||
test('mixins', () => {
|
||||
const calls: string[] = []
|
||||
const mixinA = {
|
||||
const mixinA = defineComponent({
|
||||
data() {
|
||||
return {
|
||||
a: 1
|
||||
|
@ -682,8 +682,8 @@ describe('api: options', () => {
|
|||
mounted() {
|
||||
calls.push('mixinA mounted')
|
||||
}
|
||||
}
|
||||
const mixinB = {
|
||||
})
|
||||
const mixinB = defineComponent({
|
||||
props: {
|
||||
bP: {
|
||||
type: String
|
||||
|
@ -705,7 +705,7 @@ describe('api: options', () => {
|
|||
mounted() {
|
||||
calls.push('mixinB mounted')
|
||||
}
|
||||
}
|
||||
})
|
||||
const mixinC = defineComponent({
|
||||
props: ['cP1', 'cP2'],
|
||||
data() {
|
||||
|
@ -727,7 +727,7 @@ describe('api: options', () => {
|
|||
props: {
|
||||
aaa: String
|
||||
},
|
||||
mixins: [defineComponent(mixinA), defineComponent(mixinB), mixinC],
|
||||
mixins: [mixinA, mixinB, mixinC],
|
||||
data() {
|
||||
return {
|
||||
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', () => {
|
||||
const calls: string[] = []
|
||||
|
||||
|
@ -863,7 +879,7 @@ describe('api: options', () => {
|
|||
|
||||
test('extends', () => {
|
||||
const calls: string[] = []
|
||||
const Base = {
|
||||
const Base = defineComponent({
|
||||
data() {
|
||||
return {
|
||||
a: 1,
|
||||
|
@ -878,9 +894,9 @@ describe('api: options', () => {
|
|||
expect(this.b).toBe(2)
|
||||
calls.push('base')
|
||||
}
|
||||
}
|
||||
})
|
||||
const Comp = defineComponent({
|
||||
extends: defineComponent(Base),
|
||||
extends: Base,
|
||||
data() {
|
||||
return {
|
||||
b: 2
|
||||
|
@ -900,7 +916,7 @@ describe('api: options', () => {
|
|||
|
||||
test('extends with mixins', () => {
|
||||
const calls: string[] = []
|
||||
const Base = {
|
||||
const Base = defineComponent({
|
||||
data() {
|
||||
return {
|
||||
a: 1,
|
||||
|
@ -916,8 +932,8 @@ describe('api: options', () => {
|
|||
expect(this.c).toBe(2)
|
||||
calls.push('base')
|
||||
}
|
||||
}
|
||||
const Mixin = {
|
||||
})
|
||||
const Mixin = defineComponent({
|
||||
data() {
|
||||
return {
|
||||
b: true,
|
||||
|
@ -930,10 +946,10 @@ describe('api: options', () => {
|
|||
expect(this.c).toBe(2)
|
||||
calls.push('mixin')
|
||||
}
|
||||
}
|
||||
})
|
||||
const Comp = defineComponent({
|
||||
extends: defineComponent(Base),
|
||||
mixins: [defineComponent(Mixin)],
|
||||
extends: Base,
|
||||
mixins: [Mixin],
|
||||
data() {
|
||||
return {
|
||||
c: 2
|
||||
|
|
|
@ -7,7 +7,8 @@ import {
|
|||
ComponentOptionsMixin,
|
||||
RenderFunction,
|
||||
ComponentOptionsBase,
|
||||
ComponentInjectOptions
|
||||
ComponentInjectOptions,
|
||||
ComponentOptions
|
||||
} from './componentOptions'
|
||||
import {
|
||||
SetupContext,
|
||||
|
@ -17,10 +18,11 @@ import {
|
|||
import {
|
||||
ExtractPropTypes,
|
||||
ComponentPropsOptions,
|
||||
ExtractDefaultPropTypes
|
||||
ExtractDefaultPropTypes,
|
||||
ComponentObjectPropsOptions
|
||||
} from './componentProps'
|
||||
import { EmitsOptions, EmitsToProps } from './componentEmits'
|
||||
import { isFunction } from '@vue/shared'
|
||||
import { extend, isFunction } from '@vue/shared'
|
||||
import { VNodeProps } from './vnode'
|
||||
import {
|
||||
CreateComponentPublicInstance,
|
||||
|
@ -86,12 +88,34 @@ export type DefineComponent<
|
|||
|
||||
// overload 1: direct setup function
|
||||
// (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: (
|
||||
props: Readonly<Props>,
|
||||
ctx: SetupContext
|
||||
) => RawBindings | RenderFunction
|
||||
): DefineComponent<Props, RawBindings>
|
||||
props: Props,
|
||||
ctx: SetupContext<E>
|
||||
) => RenderFunction | Promise<RenderFunction>,
|
||||
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
|
||||
// (uses user defined props interface)
|
||||
|
@ -198,6 +222,11 @@ export function defineComponent<
|
|||
): DefineComponent<PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE>
|
||||
|
||||
// implementation, close to no-op
|
||||
export function defineComponent(options: unknown) {
|
||||
return isFunction(options) ? { setup: options, name: options.name } : options
|
||||
export function defineComponent(
|
||||
options: unknown,
|
||||
extraOptions?: ComponentOptions
|
||||
) {
|
||||
return isFunction(options)
|
||||
? extend({}, extraOptions, { setup: options, name: options.name })
|
||||
: options
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue