Skip to content

Conversation

@danyadev
Copy link

@danyadev danyadev commented Mar 30, 2025

fixes #13763, fixes #11374
relates to #12761 (comment), #7963 (comment)

Problem

The "support" for generics in defineComponent was introduced in #7963, but it simply doesn't work: when you pass the props option, TypeScript ignores the main props definition and infers the props type from the props option instead.

Unfortunately, this option doesn't provide any useful type hints except for the names of the props, so all the props become any

Here is a simple example, where instead of expected normal types we encounter any:

https://play.vuejs.org/#eNp9Ustu2zAQ/JUFL1IAQ0bR9qLKAtogh/bQBIlvYRAY0lphQi8FklIcCPr3LCnLeSIniTO7s7OPQfxu26zvUOSicJVVrQe9oWYlhXd7KUpJatca62GAGreK8NTwm5A8jLC1ZgcJZye/JEnyTy3ChTWtK9YlrGCQBOBQY+WxzmEdnoZO71gfc0hN65Uhxk9gVUJvVC1pDDqVIefhKiayzLu6abEG3Huk2oHzVlFTpm0omh9rR8FY3aLvLEEakaJWffniZ4hZ2QyMxTLw7GEx5R5Er5M5IllAMvtPbjjwJLjFfZwPu9x0On7fuJ1KfzSTBgQmT9MvPw49zwV5C1tjpDhObTWkMxdFwqSMxkyb5oUYYXlQnDsCYKfBbbGcdsyYWPCGOX+rmuzeGeIDiCalqNi70mjP436cFDyqSU+Kjdbm8V/EvO1wMePVHVYPn+D34Yhy/rmw6ND2KMWR8xvboJ/os6v/vNNX5M7UneboL8hL5N674HEK+9NRzbZfxUW3f+P98pms3Vk4Gzc3FYyGSL65kadx26MNHA/ie/Yz+/ZDjM+7YwkP

type Props<T> = {
  selected: T
  onChange: (option: T) => void
}

const Select = defineComponent(<T extends string>(props: Props<T>) => {
  return () => <div>selected: {props.selected}</div>
}, {
  props: ['selected', 'onChange']
})

Solution

Let's look at one of the overloads of defineComponent:

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>

Here we can see the Props generic type, the setup argument using Props and the options argument also using Props

When we add a generic type to the setup function, it becomes less obvious for TypeScript to decide which variable to use to infer the generic type, and unfortunately for us it chooses the variant with less type density.

So we need to tell TypeScript not to infer Props from the options.props field:

props?: (keyof NoInfer<Props>)[]

Another solution

Initially I've come up with another solution which doesn't rely on that new TypeScript NoInfer type. But as you already use NoInfer in runtime code, it may be irrelevant.

It works by separating Props into two generics — original Props and DeclaredProps — and using them differently:

export function defineComponent<
  Props extends Record<string, any>,
  ...,
  DeclaredProps extends (keyof Props)[] = (keyof Props)[],
>(
  setup: (props: Props, ...) => ...,
  options?: Pick<ComponentOptions, 'name' | 'inheritAttrs'> & {
    props?: DeclaredProps
    ...
  },
): DefineSetupFnComponent<Props, E, S>

A note about an object format for props with runtime validations

This MR fixes only the usage with props defined as an array of strings. I haven't found any solution for the object format and I'm not sure that there is one...

But on the other side, I don't think someone would need to combine generics with runtime validations

Summary by CodeRabbit

  • Tests

    • Expanded test coverage for function-style components using runtime props: added named component variants, richer JSX usage, generic and non-generic scenarios, and explicit compile-time error assertions (including the known limitation when mixing generics with object-style runtime props).
  • New Features

    • Refined type inference for component runtime props to avoid unintended eager inference, improving type safety and developer experience.

@github-actions
Copy link

github-actions bot commented Mar 31, 2025

Size Report

Bundles

File Size Gzip Brotli
runtime-dom.global.prod.js 101 kB (-182 B) 38.2 kB (-64 B) 34.4 kB (-41 B)
vue.global.prod.js 159 kB (-182 B) 58.4 kB (-66 B) 52 kB (-22 B)

Usages

Name Size Gzip Brotli
createApp (CAPI only) 46.6 kB (-1 B) 18.2 kB (-1 B) 16.7 kB
createApp 54.5 kB (-7 B) 21.2 kB (-7 B) 19.4 kB (+2 B)
createSSRApp 58.7 kB (-7 B) 23 kB (-8 B) 20.9 kB (-6 B)
defineCustomElement 59.3 kB (-145 B) 22.8 kB (-42 B) 20.8 kB (-39 B)
overall 68.6 kB (-7 B) 26.4 kB (-6 B) 24 kB (-76 B)

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 31, 2025

Open in StackBlitz

@vue/compiler-core

npm i https://pkg.pr.new/@vue/compiler-core@13119

@vue/compiler-dom

npm i https://pkg.pr.new/@vue/compiler-dom@13119

@vue/compiler-sfc

npm i https://pkg.pr.new/@vue/compiler-sfc@13119

@vue/compiler-ssr

npm i https://pkg.pr.new/@vue/compiler-ssr@13119

@vue/reactivity

npm i https://pkg.pr.new/@vue/reactivity@13119

@vue/runtime-core

npm i https://pkg.pr.new/@vue/runtime-core@13119

@vue/runtime-dom

npm i https://pkg.pr.new/@vue/runtime-dom@13119

@vue/server-renderer

npm i https://pkg.pr.new/@vue/server-renderer@13119

@vue/shared

npm i https://pkg.pr.new/@vue/shared@13119

vue

npm i https://pkg.pr.new/vue@13119

@vue/compat

npm i https://pkg.pr.new/@vue/compat@13119

commit: 5e1f00a

@danyadev danyadev force-pushed the fix-generic-defineComponent-with-runtime-props branch from cf9276c to d7d0e0e Compare May 15, 2025 20:28
@coderabbitai
Copy link

coderabbitai bot commented May 15, 2025

Walkthrough

Adjusted a defineComponent overload to use NoInfer<Props> for props keys to avoid eager inference, and expanded DTS tests with named function-syntax components (Comp1/Comp2/Comp3) covering array/object runtime props, generics, JSX usage, and expected TypeScript errors.

Changes

Cohort / File(s) Change Summary
Runtime API overload
packages/runtime-core/src/apiDefineComponent.ts
Updated first defineComponent overload: props?: (keyof Props)[]props?: (keyof NoInfer<Props>)[] to prevent eager inference of Props.
Type tests (dts)
packages-private/dts-test/defineComponent.test-d.tsx
Added named function-syntax components Comp1, Comp2, Comp3 and tests covering array vs object runtime props, generics (including Comp2<T> usage), TSX usage, prop key/type mismatches, missing/extraneous props, and @ts-expect-error assertions for unsupported combinations (e.g., generics + object runtime props).

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Dev as Developer (TSX)
    participant API as defineComponent (runtime-core)
    participant TS as TypeScript Checker

    Note over Dev,API: Define component (maybe generic) + runtime props (array|object)
    Dev->>API: call defineComponent(setupFn, { props: [...] / { ... } })
    API->>TS: expose component type (uses NoInfer for props keys)
    TS-->>API: infer/resolve generic Params and prop typings
    Dev->>TS: use component in JSX (with or without explicit generic)
    TS-->>Dev: validate props, emit diagnostics or accept usage
    Note over TS,Dev: @ts-expect-error annotations validate expected failures
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

:hammer: p3-minor-bug

Poem

A rabbit tests beneath the moon,
Generics hop and props attune.
Named comps prance through JSX light,
Type errors flagged in gentle sight.
🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly indicates a type system fix for generics with runtime props in defineComponent, matching the core of the changeset and referencing the relevant issue #11374, making it descriptive and on-point without unnecessary verbosity.
Linked Issues Check ✅ Passed This PR updates the defineComponent overload to use NoInfer on props array to prevent erasure of generic Props and adds TSX tests demonstrating generic parameter propagation with runtime props declarations, directly addressing the linked issue’s requirement to preserve strong typing for generics when using the array-form props API.
Out of Scope Changes Check ✅ Passed All modifications are confined to updating the defineComponent type overload and adding related test cases in the DTS suite to validate generic propagation with array-form runtime props, which directly correlates with the linked issue’s scope and does not introduce any unrelated functionality.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@danyadev danyadev changed the title fix: make generics with runtime props in defineComponent work (fix #11374) fix(types): make generics with runtime props in defineComponent work (fix #11374) May 15, 2025
@danyadev danyadev force-pushed the fix-generic-defineComponent-with-runtime-props branch from d7d0e0e to 5e1f00a Compare May 15, 2025 20:38
@danyadev
Copy link
Author

danyadev commented May 15, 2025

Hi @jh-leong! Rebased the MR on the main branch to pick up all the changes from the released version, but it seems that the pkg-pr-new bot doesn't want to update its builds, and maybe you know how to bump it?

upd: it updated the builds, it seems like the pipelines were a bit clogged at the time and didn't show that they were in progress

Also updated the description and hopefully made it fresher and easier to understand, plus added the link to the playground!

Really need this feature, so I've been using it right from the creation of this MR :)

) => RenderFunction | Promise<RenderFunction>,
options?: Pick<ComponentOptions, 'name' | 'inheritAttrs'> & {
props?: (keyof Props)[]
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.

@jh-leong jh-leong added the 🍰 p2-nice-to-have Priority 2: this is not breaking anything but nice to have it addressed. label Jun 10, 2025
@danyadev danyadev force-pushed the fix-generic-defineComponent-with-runtime-props branch from 5e1f00a to b8cc94b Compare June 11, 2025 17:28
@vuejs vuejs deleted a comment from jh-leong Jun 12, 2025
@vuejs vuejs deleted a comment from jh-leong Jun 12, 2025
@vuejs vuejs deleted a comment from jh-leong Jun 12, 2025
@jh-leong jh-leong self-requested a review June 12, 2025 06:34
@cernymatej
Copy link

@danyadev sorry for the ping, just wondering what the status of this PR is 🙏

I can see that some tests in the language tools failed

@danyadev danyadev force-pushed the fix-generic-defineComponent-with-runtime-props branch from b8cc94b to 4de7ac9 Compare September 2, 2025 16:37
@danyadev
Copy link
Author

danyadev commented Sep 2, 2025

Hi @cernymatej! I've looked through these CI failures and most of them seem to be unrelated. Maybe some snapshots in language-tools need to be regenerated due to a new overload being added, but as a first step I'll suggest just re-running the tests after pulling the main branch

Though I can't run CI tests myself, we need a staff member to do so, for example @jh-leong, but he seems to have little activity lately :c

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
packages-private/dts-test/defineComponent.test-d.tsx (3)

1424-1431: Clarify subset relationship between manual props type and runtime keys.

The second case is allowed (manual type may be a superset; runtime props narrows accepted external keys). Add an inline comment to avoid confusion with the earlier “must match” wording.

Apply this diff to document intent:

 defineComponent(
   (_props: { msg: string; bar: string }) => {
     return () => {}
   },
   {
+    // OK: manual props type can be a superset; runtime-only accepts 'msg'
     props: ['msg'],
   },
 )

1441-1448: Strengthen the generic-propagation checks for Comp2.

Add a local assertion to ensure T flows into setup props, and a TSX case that rejects an explicit wrong generic argument.

 const Comp2 = defineComponent(
-  <T extends string>(_props: { msg: T }) => {
-    return () => {}
-  },
+  <T extends string>(_props: { msg: T }) => {
+    // T should flow through, guarding against regression to `any`
+    expectType<T>(_props.msg)
+    return () => {}
+  },
   {
     props: ['msg'],
   },
 )
 
 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 explicit wrong generic argument violates `T extends string`
+expectType<JSX.Element>(<Comp2<number> 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" />)

Also applies to: 1469-1476


1460-1467: Mirror the subset clarification for generic case.

Like the non-generic case, consider an inline comment noting that manual props may be a superset while runtime narrows accepted keys.

 defineComponent(
   <T extends string>(_props: { msg: T; bar: T }) => {
     return () => {}
   },
   {
+    // OK: manual includes 'bar', but runtime only accepts 'msg'
     props: ['msg'],
   },
 )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b8cc94b and 4de7ac9.

📒 Files selected for processing (2)
  • packages-private/dts-test/defineComponent.test-d.tsx (4 hunks)
  • packages/runtime-core/src/apiDefineComponent.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/runtime-core/src/apiDefineComponent.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages-private/dts-test/defineComponent.test-d.tsx (1)
packages/runtime-core/src/apiDefineComponent.ts (1)
  • defineComponent (305-315)
🔇 Additional comments (7)
packages-private/dts-test/defineComponent.test-d.tsx (7)

1405-1412: Good positive coverage for array runtime props (non-generic).

Comp1 and its TSX assertions correctly validate the happy path and guard against “any” regressions via the wrong-type and missing-prop cases.

Also applies to: 1433-1440


1414-1422: Negative case for extra runtime keys is correct.

This catches the scenario where options.props includes a key not present in the manual props type.


1450-1458: Correct negative case for extra runtime keys with generics.

This ensures NoInfer keeps inference from the array from corrupting the generic props type.


1478-1488: Object runtime props: limitation is well captured.

Tests clearly encode that generics aren’t supported with object-form runtime props and that explicit type arguments should error in TSX.

Also applies to: 1514-1523


1491-1501: Negative cases for object runtime props are correct.

Both “missing in manual type” and “generic + object props” error scenarios are asserted properly.

Also applies to: 1503-1512


1525-1532: Name mismatch test looks good.

Covers the “string prop names don’t match” edge.


1534-1544: Type mismatch test looks good.

Asserting Number vs string type mismatch prevents silent widening.

@jh-leong
Copy link
Member

jh-leong commented Sep 3, 2025

/ecosystem-ci run

@vuejs vuejs deleted a comment from jh-leong Sep 3, 2025
@vue-bot
Copy link
Contributor

vue-bot commented Sep 3, 2025

📝 Ran ecosystem CI: Open

suite result latest scheduled
pinia success success
primevue failure success
nuxt success success
vite-plugin-vue success success
router success success
quasar success success
vant success success
language-tools failure success
vuetify success success
vue-macros success failure
radix-vue success success
vue-i18n success success
test-utils success success
vueuse success success
vitepress success success
vue-simple-compiler success success

@danyadev danyadev force-pushed the fix-generic-defineComponent-with-runtime-props branch from 4de7ac9 to 5571da1 Compare September 29, 2025 11:50
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages-private/dts-test/defineComponent.test-d.tsx (2)

1442-1448: Strengthen generic preservation with a direct assertion inside setup

To ensure T isn’t widened (i.e., not inferred as any) when array runtime props are present, add a local check that _props.msg is exactly T.

Apply this minimal diff:

 const Comp2 = defineComponent(
   <T extends string>(_props: { msg: T }) => {
+    expectType<T>(_props.msg)
     return () => {}
   },
   {
     props: ['msg'],
   },
 )

1450-1458: Negative generic cases look right; add one constraint error for completeness

  • The “extra key” and “missing in runtime” checks are on point.
  • Optional: add an explicit generic-constraint failure to prove TS won’t allow <number> for T extends string.

You can extend the TSX assertions near Comp2 with:

   expectType<JSX.Element>(<Comp2 msg="1" />)
   expectType<JSX.Element>(<Comp2<string> msg="1" />)
+  // @ts-expect-error T extends string, number is not allowed
+  expectType<JSX.Element>(<Comp2<number> msg={1} />)

Also applies to: 1460-1468

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4de7ac9 and 5571da1.

📒 Files selected for processing (2)
  • packages-private/dts-test/defineComponent.test-d.tsx (4 hunks)
  • packages/runtime-core/src/apiDefineComponent.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/runtime-core/src/apiDefineComponent.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages-private/dts-test/defineComponent.test-d.tsx (1)
packages/runtime-core/src/apiDefineComponent.ts (1)
  • defineComponent (305-315)
🔇 Additional comments (3)
packages-private/dts-test/defineComponent.test-d.tsx (3)

1405-1413: Array runtime props with manual types: good coverage and intent is clear

  • Positive and negative cases (extra key, missing key, wrong TSX prop type) look correct and align with the NoInfer behavior you’re enforcing. No issues spotted.

Also applies to: 1414-1423, 1424-1432, 1433-1440


1479-1488: Explicitly documenting “object runtime props + generics = unsupported” is valuable

  • The tests correctly assert that generics aren’t supported with object-format runtime props, and the negative cases (missing keys, explicit generic usage) are precise.
  • No changes requested.

Also applies to: 1491-1500, 1502-1512, 1514-1523


1524-1533: Mismatched names and type-mismatch checks are precise

  • Great edge coverage for incorrect key names and validator-type mismatches.

Also applies to: 1535-1544

@danyadev
Copy link
Author

danyadev commented Sep 29, 2025

Hi @edison1105, can you please approve the ci workflow to run pkg-pr-new? That way I'll be able to grab the latest vue version with this MR's code included

Though it would be much better to make progress with this MR, and your help would be much appreciated. We stumbled on some ecosystem CI failures because of the changes in TS declarations. To fix them, we need to update affected .d.ts files, but I don't know how to do it correctly across the repositories

upd: published the package myself: https://pkg.pr.new/danyadev/vuejs-core/vue@37fda96

@auvred
Copy link
Contributor

auvred commented Nov 8, 2025

The currently linked issue (#11374) is about passing type arguments explicitly to a component, like <Comp<string> prop="value" />. This PR actually fixes a different issue - #13763. I believe that one should be linked instead.

@danyadev
Copy link
Author

danyadev commented Nov 8, 2025

This PR fixes both issues, because it introduces support for passing generics to components too. The second issue was created after this PR was created, so I didn't know about it. I'll mark it to be fixed too, thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🍰 p2-nice-to-have Priority 2: this is not breaking anything but nice to have it addressed. scope: types

Projects

None yet

Development

Successfully merging this pull request may close these issues.

props type of generic defineComponent is inferred to be any How to use generics in TSX

6 participants