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', () => {
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() {

View File

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

View File

@ -122,7 +122,7 @@ describe('api: options', () => {
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({
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

View File

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