Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(types): defineComponent() with generics support #7963

Merged
merged 3 commits into from Mar 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 })
yyx990803 marked this conversation as resolved.
Show resolved Hide resolved
: options
}