]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(types): `defineComponent()` with generics support (#7963)
authorEvan You <yyx990803@gmail.com>
Mon, 27 Mar 2023 10:28:43 +0000 (18:28 +0800)
committerGitHub <noreply@github.com>
Mon, 27 Mar 2023 10:28:43 +0000 (18:28 +0800)
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

packages/dts-test/defineComponent.test-d.tsx
packages/dts-test/h.test-d.ts
packages/runtime-core/__tests__/apiOptions.spec.ts
packages/runtime-core/src/apiDefineComponent.ts

index 522b6a8daae805809d76f8dc4d83da2748670476..1b981a87cb067d6d5023c9b461fe5b87d9f4d707 100644 (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() {
index 92246f0f96b042ad9f4986156598cc0ba075664e..5c700800e94a30c60fed740ca2c079e690ccd5ec 100644 (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
index e658afa07010e73ce44960f844d8a54e4b28726c..eebf0bacffb639183fdd69db20750371c5e74645 100644 (file)
@@ -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
index c10ac74e4a96304462f3587e76da819b6c179343..bf579e7557357d8ade31410bda74deac879b7381 100644 (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: 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)
@@ -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
 }