Skip to content

Commit

Permalink
feat(types): defineComponent() with generics support (#7963)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
yyx990803 committed Mar 27, 2023
1 parent 9a8073d commit d77557c
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 28 deletions.
124 changes: 122 additions & 2 deletions packages/dts-test/defineComponent.test-d.tsx
Expand Up @@ -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 />
Expand Down Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion packages/dts-test/h.test-d.ts
Expand Up @@ -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
Expand Down
46 changes: 31 additions & 15 deletions packages/runtime-core/__tests__/apiOptions.spec.ts
Expand Up @@ -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() {
Expand Down Expand Up @@ -667,7 +667,7 @@ describe('api: options', () => {

test('mixins', () => {
const calls: string[] = []
const mixinA = {
const mixinA = defineComponent({
data() {
return {
a: 1
Expand All @@ -682,8 +682,8 @@ describe('api: options', () => {
mounted() {
calls.push('mixinA mounted')
}
}
const mixinB = {
})
const mixinB = defineComponent({
props: {
bP: {
type: String
Expand All @@ -705,7 +705,7 @@ describe('api: options', () => {
mounted() {
calls.push('mixinB mounted')
}
}
})
const mixinC = defineComponent({
props: ['cP1', 'cP2'],
data() {
Expand All @@ -727,7 +727,7 @@ describe('api: options', () => {
props: {
aaa: String
},
mixins: [defineComponent(mixinA), defineComponent(mixinB), mixinC],
mixins: [mixinA, mixinB, mixinC],
data() {
return {
c: 4,
Expand Down Expand Up @@ -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[] = []

Expand Down Expand Up @@ -863,7 +879,7 @@ describe('api: options', () => {

test('extends', () => {
const calls: string[] = []
const Base = {
const Base = defineComponent({
data() {
return {
a: 1,
Expand All @@ -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
Expand All @@ -900,7 +916,7 @@ describe('api: options', () => {

test('extends with mixins', () => {
const calls: string[] = []
const Base = {
const Base = defineComponent({
data() {
return {
a: 1,
Expand All @@ -916,8 +932,8 @@ describe('api: options', () => {
expect(this.c).toBe(2)
calls.push('base')
}
}
const Mixin = {
})
const Mixin = defineComponent({
data() {
return {
b: true,
Expand All @@ -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
Expand Down
49 changes: 39 additions & 10 deletions packages/runtime-core/src/apiDefineComponent.ts
Expand Up @@ -7,7 +7,8 @@ import {
ComponentOptionsMixin,
RenderFunction,
ComponentOptionsBase,
ComponentInjectOptions
ComponentInjectOptions,
ComponentOptions
} from './componentOptions'
import {
SetupContext,
Expand All @@ -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,
Expand Down Expand Up @@ -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: 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: Readonly<Props>,
ctx: SetupContext
) => RawBindings | RenderFunction
): DefineComponent<Props, RawBindings>
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)
Expand Down Expand Up @@ -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
}

0 comments on commit d77557c

Please sign in to comment.