Skip to content

fix(language-core): make generic component internal context inference type-safe across .d.ts boundary#6104

Merged
KazariEX merged 2 commits into
vuejs:masterfrom
Holiden:fix/generic-component-ctx-nonnullable
Jun 20, 2026
Merged

fix(language-core): make generic component internal context inference type-safe across .d.ts boundary#6104
KazariEX merged 2 commits into
vuejs:masterfrom
Holiden:fix/generic-component-ctx-nonnullable

Conversation

@Holiden

@Holiden Holiden commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

What

In generateGeneric (packages/language-core/lib/codegen/script/scriptSetup.ts), the emitted return type of a generic component uses __ctx?: Awaited<typeof __VLS_setup> without a NonNullable wrapper, while the props, ctx and exposed parameter positions already wrap it as NonNullable<Awaited<typeof __VLS_setup>>. This wraps __ctx to match.

- { __ctx?: Awaited<typeof __VLS_setup> }
+ { __ctx?: NonNullable<Awaited<typeof __VLS_setup>> }

Why

Event-handler parameters on a generic component are typed as implicit any (TS7006 under noImplicitAny) in a consumer when the component is consumed across the emitted .d.ts boundary (a built package / Nuxt layer), even though slot props are inferred correctly.

In the live virtual code __VLS_setup is a parameter with a default value, so typeof __VLS_setup has no undefined. On declaration emit, TypeScript turns the defaulted parameter into an optional one (__VLS_setup?: Promise<…>), so typeof __VLS_setup now includes undefined, and Awaited<Promise<Setup> | undefined> becomes Setup | undefined. The consumer helper __VLS_FunctionalComponentProps extracts props via a single-step nested inference K extends { __ctx?: { props?: infer P } }, which cannot infer P through the inner | undefined, so props resolves to never. With never props, __VLS_NormalizeComponentEvent falls back and the event handler loses its contextual type → implicit any.

Slot props are unaffected because they go through __VLS_FunctionalComponentCtx, which infers the whole __ctx and applies NonNullable to it. This change makes the return-type __ctx consistent with the parameter positions and with that helper.

This completes #4577, which added the same NonNullable wrapping to the parameter positions but left the return-type __ctx.

Reproduction

Reproduces only across the emitted .d.ts boundary (a same-project live-source test does not trigger it):

  1. A <script setup generic="T"> component with defineEmits<{ change: [e: SomeType] }>().
  2. Emit its declaration (vue-tsc --declaration --emitDeclarationOnly --noCheck).
  3. A consumer that imports the emitted .d.ts and writes @change="(e) => …".
  4. vue-tsc --noEmit under noImplicitAny reports TS7006 on e without this change, and is clean with it.

No runtime impact: in the live (non-emitted) path the wrapper is a no-op because typeof __VLS_setup has no undefined.

@KazariEX KazariEX changed the title fix(language-core): wrap generic component __ctx return type in NonNullable fix(language-core): make generic component internal context inference type-safe across .d.ts boundary Jun 20, 2026
@KazariEX KazariEX merged commit bff182a into vuejs:master Jun 20, 2026
4 checks passed
@Holiden Holiden deleted the fix/generic-component-ctx-nonnullable branch June 20, 2026 11:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants