core icon indicating copy to clipboard operation
core copied to clipboard

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

Open danyadev opened this issue 8 months ago • 6 comments

fixes #11374 relates to https://github.com/vuejs/core/issues/12761#issuecomment-2764608203, https://github.com/vuejs/core/pull/7963#issuecomment-2246825089

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

Summary by CodeRabbit

  • Tests

    • Expanded and refined test coverage for component definitions with runtime props, including generics and TypeScript error expectations.
    • Added JSX usage tests verifying correct and incorrect prop usage and type checking.
  • New Features

    • Enhanced type inference for component runtime props with a new function overload using a utility type to prevent premature generic type inference.

danyadev avatar Mar 30 '25 23:03 danyadev

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)

github-actions[bot] avatar Mar 31 '25 03:03 github-actions[bot]

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

pkg-pr-new[bot] avatar Mar 31 '25 03:03 pkg-pr-new[bot]

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

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.

coderabbitai[bot] avatar May 15 '25 20:05 coderabbitai[bot]

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 :)

danyadev avatar May 15 '25 21:05 danyadev

@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

cernymatej avatar Sep 02 '25 16:09 cernymatej

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

danyadev avatar Sep 02 '25 16:09 danyadev

/ecosystem-ci run

jh-leong avatar Sep 03 '25 09:09 jh-leong

📝 Ran ecosystem CI: Open

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

vue-bot avatar Sep 03 '25 09:09 vue-bot

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

danyadev avatar Sep 29 '25 13:09 danyadev

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.

auvred avatar Nov 08 '25 13:11 auvred

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

danyadev avatar Nov 08 '25 14:11 danyadev