Skip to content

fix(types): make generics with runtime props in defineComponent work (fix #11374) #13119

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 103 additions & 9 deletions packages-private/dts-test/defineComponent.test-d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
type Component,
type ComponentOptions,
type ComponentPublicInstance,
type DefineSetupFnComponent,
type PropType,
type SetupContext,
type Slots,
Expand Down Expand Up @@ -1402,16 +1403,53 @@ describe('function syntax w/ emits', () => {
describe('function syntax w/ runtime props', () => {
// with runtime props, the runtime props must match
// manual type declaration
const Comp1 = defineComponent(
(_props: { msg: string }) => {
return () => {}
},
{
props: ['msg'],
},
)

expectType<JSX.Element>(<Comp1 msg="1" />)
// @ts-expect-error msg type is incorrect
expectType<JSX.Element>(<Comp1 msg={1} />)
// @ts-expect-error msg is missing
expectType<JSX.Element>(<Comp1 />)
// @ts-expect-error bar doesn't exist
expectType<JSX.Element>(<Comp1 msg="1" bar="2" />)

// @ts-expect-error bar isn't specified in props definition
defineComponent(
(_props: { msg: string }) => {
return () => {}
},
{
props: ['msg', 'bar'],
},
)

defineComponent(
(_props: { msg: string; bar: string }) => {
return () => {}
},
{
props: ['msg'],
},
)

// @ts-expect-error string prop names don't match
defineComponent(
(_props: { msg: string }) => {
return () => {}
},
{
props: ['bar'],
},
)

const Comp2 = defineComponent(
<T extends string>(_props: { msg: T }) => {
return () => {}
},
Expand All @@ -1420,7 +1458,42 @@ describe('function syntax w/ runtime props', () => {
},
)

expectType<JSX.Element>(<Comp2 msg="1" />)
expectType<JSX.Element>(<Comp2<string> msg="1" />)
// @ts-expect-error msg type is incorrect
expectType<JSX.Element>(<Comp2 msg={1} />)
// @ts-expect-error msg is missing
expectType<JSX.Element>(<Comp2 />)
// @ts-expect-error bar doesn't exist
expectType<JSX.Element>(<Comp2 msg="1" bar="2" />)

const Comp3 = defineComponent(
<T extends string>(_props: { msg: T }) => {
return () => {}
},
{
props: ['msg', 'bar'],
},
)

// This is not the preferred behavior because it's better to see a typescript error,
// but this is a compromise to resolve a relatively worse problem -
// not inferring props types from runtime props when the types are not explicitly set.
// See #13119#discussion_r2137831991
expectType<DefineSetupFnComponent<{ msg: any; bar: any }>>(Comp3)

defineComponent(
<T extends string>(_props: { msg: T; bar: T }) => {
return () => {}
},
{
props: ['msg'],
},
)

// Note: generics aren't supported with object runtime props
// so the props will infer the runtime props' types
const Comp4 = defineComponent(
<T extends string>(_props: { msg: T }) => {
return () => {}
},
Expand All @@ -1431,13 +1504,26 @@ describe('function syntax w/ runtime props', () => {
},
)

// @ts-expect-error string prop names don't match
expectType<DefineSetupFnComponent<{ msg: string }>>(Comp4)
expectType<JSX.Element>(<Comp4 msg="1" />)
// @ts-expect-error generics aren't supported with object runtime props
expectType<JSX.Element>(<Comp4<string> msg="1" />)
// @ts-expect-error msg type is incorrect
expectType<JSX.Element>(<Comp4 msg={1} />)
// @ts-expect-error msg is missing
expectType<JSX.Element>(<Comp4 />)
// @ts-expect-error bar doesn't exist
expectType<JSX.Element>(<Comp4 msg="1" bar="2" />)

defineComponent(
(_props: { msg: string }) => {
// @ts-expect-error bar isn't specified in props definition
<T extends string>(_props: { msg: T }) => {
return () => {}
},
{
props: ['bar'],
props: {
bar: String,
},
},
)

Expand All @@ -1453,16 +1539,24 @@ describe('function syntax w/ runtime props', () => {
},
)

// @ts-expect-error prop keys don't match
const Comp5 = defineComponent(
_props => {
return () => {}
},
{
props: ['foo'],
},
)

expectType<DefineSetupFnComponent<{ foo: any }>>(Comp5)

defineComponent(
(_props: { msg: string }, ctx) => {
// @ts-expect-error the props type is required when a generic type is present
<T,>(_props) => {
return () => {}
},
{
props: {
msg: String,
bar: String,
},
props: [],
},
)
})
Expand Down
16 changes: 16 additions & 0 deletions packages/runtime-core/src/apiDefineComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,22 @@ export function defineComponent<
slots?: S
},
): DefineSetupFnComponent<Props, E, S>
export function defineComponent<
Props extends Record<string, any>,
E extends EmitsOptions = {},
EE extends string = string,
S extends SlotsType = {},
>(
setup: (
props: Props,
ctx: SetupContext<E, S>,
) => RenderFunction | Promise<RenderFunction>,
options?: Pick<ComponentOptions, 'name' | 'inheritAttrs'> & {
props?: (keyof NoInfer<Props>)[]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One concern, to avoid breaking this case:

defineComponent(
  props => {
    // Before: { msg: any }
    // After : Record<string, any>

    // @ts-expect-error: should error when accessing undefined props
    props.foo

    // auto-completion missing for props
    props.msg

    return () => {}
  },
  {
    props: ['msg']
  }
)

We might want to keep the original behavior by adding a new overload instead:

// overload 1: direct setup function
// (uses user defined props interface)
export function defineComponent<
  Props extends Record<string, any>,
  E extends EmitsOptions = {},
  EE extends string = string,
  S extends SlotsType = {},
>(
  setup: (
    props: Props,
    ctx: SetupContext<E, S>,
  ) => RenderFunction | Promise<RenderFunction>,
  options?: Pick<ComponentOptions, 'name' | 'inheritAttrs'> & {
    props?: (keyof Props)[]
    emits?: E | EE[]
    slots?: S
  },
): DefineSetupFnComponent<Props, E, S>
+export function defineComponent<
+  Props extends Record<string, any>,
+  E extends EmitsOptions = {},
+  EE extends string = string,
+  S extends SlotsType = {},
+>(
+  setup: (
+    props: Props,
+    ctx: SetupContext<E, S>,
+  ) => RenderFunction | Promise<RenderFunction>,
+  options?: Pick<ComponentOptions, 'name' | 'inheritAttrs'> & {
+    props?: (keyof NoInfer<Props>)[]
+    emits?: E | EE[]
+    slots?: S
+  },
+)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, but the proposed solution breaks another thing: if runtime props contain more props than declared in props, and the function is generic, the types fall back to any:

image image

(@ts-expect-error is red because there was an error, but it's gone now)

It's still the best solution though, I've tried some variants and none of them worked

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applied your solution for now and added a few tests

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this issue is negligible in practice — it only happens when props is both generic and doesn't match the runtime props, which seems rare.

emits?: E | EE[]
slots?: S
},
): DefineSetupFnComponent<Props, E, S>
export function defineComponent<
Props extends Record<string, any>,
E extends EmitsOptions = {},
Expand Down