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:
Evan You 2023-03-27 18:28:43 +08:00 committed by GitHub
parent 9a8073d0ae
commit d77557c403
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 193 additions and 28 deletions

View File

@ -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() {

View File

@ -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

View File

@ -122,7 +122,7 @@ describe('api: options', () => {
expect(serializeInner(root)).toBe(`<div>4</div>`) expect(serializeInner(root)).toBe(`<div>4</div>`)
}) })
test('components 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

View File

@ -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
} }