Skip to content

feat(core): enhance type inference for useEmitAsProps and useForwardPropsEmits#2643

Open
Myshkouski wants to merge 11 commits into
unovue:v2from
Myshkouski:feat/use-emit-as-props-typed-return
Open

feat(core): enhance type inference for useEmitAsProps and useForwardPropsEmits#2643
Myshkouski wants to merge 11 commits into
unovue:v2from
Myshkouski:feat/use-emit-as-props-typed-return

Conversation

@Myshkouski
Copy link
Copy Markdown

@Myshkouski Myshkouski commented May 14, 2026

🔗 Linked issue

#2642

❓ Type of change

  • 📖 Documentation (updates to the documentation, readme or JSdoc annotations)
  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • 👌 Enhancement (improving an existing functionality like performance)
  • ✨ New feature (a non-breaking change that adds functionality)
  • 🧹 Chore (updates to the build process or auxiliary tools and libraries)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

📚 Description

This PR enhances TypeScript type inference for the useEmitAsProps and useForwardPropsEmits composition API utilities:

useEmitAsProps now properly types the returned props based on the emit function signature:

  • Added generic type parameter Fn for the emit function type
  • Added type utilities (ToEmit, EmitAsProps, FunctionSignature, etc.) for handling function overloads
  • Result type accurately reflects event handler signatures for better IDE autocomplete

useForwardPropsEmits receives improved type inference:

  • Added function overloads to properly infer ComputedRef return types based on whether emit is provided
  • When emit is provided, the return type includes both forwarded props AND emit-as-props with correct typing
  • Exported WithOptionalBooleans type utility for external use

Changes:

  • packages/core/src/shared/useEmitAsProps.ts - Core type improvements, 81 lines added/modified
  • packages/core/src/shared/useForwardProps.ts - Export WithOptionalBooleans
  • packages/core/src/shared/useForwardPropsEmits.ts - Add function overloads for proper type inference

This change improves the developer experience when using these utilities by providing accurate TypeScript types and better IDE autocomplete for event handlers.

📸 Screenshots (if appropriate)

📝 Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly. (public API hasn't changed)

Summary by CodeRabbit

  • Refactor
    • Strengthened TypeScript typings for event-handler prop mapping and prop forwarding, improving type safety and IDE autocomplete.
    • Exported additional utility types for more precise downstream typing.
    • Added explicit overloads for the props+emit merge to clarify return types.
    • No runtime behavior changes; purely type-level improvements.

Review Change Stack

The useEmitAsProps function now properly types the returned props based on
the emit function signature, including support for function overloads. This
improves TypeScript type inference and provides better IDE autocomplete for
event handlers.

- Added generic type parameter Fn for the emit function type
- Added type utilities for handling function overloads
- Result type now accurately reflects the event handler signatures
Exported WithOptionalBooleans type utility and added function overloads
to properly infer ComputedRef return types based on emit parameter.
The overloads accurately reflect whether emits are converted to props.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 14, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Stronger TypeScript typing for emit- and prop-forwarding: useEmitAsProps is retyped and gains exported mapping types; WithOptionalBooleans is exported from useForwardProps; useForwardPropsEmits adds overloads composing forwarded props with emit-derived handler props.

Changes

Emit + Props forwarding type system

Layer / File(s) Summary
Expose forwarded-props boolean helper
packages/core/src/shared/useForwardProps.ts
WithOptionalBooleans<T> is exported from the module.
useEmitAsProps signature update
packages/core/src/shared/useEmitAsProps.ts
useEmitAsProps generic signature now accepts Fn extends AnyFn and takes emit: Emit<Name, Fn> instead of a loose (name, ...args) => void.
Emit contract and mapping utilities
packages/core/src/shared/useEmitAsProps.ts, packages/core/src/shared/index.ts
Adds and exports EmitAsProps<T> and Emit<Name, Fn> plus supporting conditional/template-literal types that map emit overloads to onXxx handler prop shapes; useEmitAsProps returns EmitAsProps<Fn> and the shared entry now re-exports type EmitAsProps.
Compose forwarded props + emits with overloads
packages/core/src/shared/useForwardPropsEmits.ts
Adds two overloads: props-only returns ComputedRef<WithOptionalBooleans<T>>; props+emit returns ComputedRef<WithOptionalBooleans<T> & EmitAsProps<Fn>>. Implementation accepts emit?: Emit<Name, Fn> and merges useForwardProps(props) with useEmitAsProps(emit) when provided.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

  • unovue/reka-ui#2561 — Prior PR touching WithOptionalBooleans<T> in useForwardProps, now exported and reused by these typings.

Poem

🐰 I hopped through types both wide and deep,

turned emits to props for callers to keep,
overloads tucked in tidy rows,
handler names where camel seed grows,
a little rabbit nods and leaps.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: enhancing type inference for two specific composition-API utilities in the core package.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Copy link
Copy Markdown
Contributor

@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: 2

🧹 Nitpick comments (3)
packages/core/src/shared/useEmitAsProps.ts (3)

55-58: 💤 Low value

@ts-expect-error on ExtendSignature deserves an inline rationale.

The trick of extending a generic type parameter (interface … extends T) is intentional and load-bearing for the recursive overload walk, but it isn't obvious to a future maintainer why TS is being silenced here. Consider expanding the comment to call out (a) why the violation is required, (b) what would silently break if the suppression is removed (e.g. the recursion bottoms out incorrectly), and (c) a hint to keep the comment if upstream TS ever permits it. This avoids someone "cleaning up" the directive and regressing the type derivation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/shared/useEmitAsProps.ts` around lines 55 - 58, Update the
existing `@ts-expect-error` comment above the `ExtendSignature` interface to
include a concise rationale: state that extending the generic type parameter `T`
is intentional to inherit all overloads for the recursive overload-walking
algorithm, explain that removing the suppression causes TypeScript to error and
would break the recursion/bottoming-out logic (silently changing overload
inference), and add a note to preserve this directive until TypeScript natively
supports this pattern (or link to an upstream issue/PR if available). Reference
`ExtendSignature<T, TArgs, TReturn>` and the recursive overload walk behavior in
the comment so future maintainers understand why the suppression must remain.

88-90: ⚡ Quick win

Rename UnionToOptional — it doesn't introduce optionality.

The mapped type collects keys across all union members and emits required properties (with never filling missing branches); it does not produce optional (?) properties. The behaviour is closer to a distributive “union-to-merged-object” / UnionToIntersection-style helper, and the current name will mislead future readers (and anyone grepping for an "optional" helper).

♻️ Suggested rename
-type UnionToOptional<T> = {
+type MergeUnion<T> = {
   [K in T extends any ? keyof T : never]: T extends { [P in K]: any } ? T[K] : never;
 }
@@
-export type EmitAsProps<T extends Function> = Props<UnionToOptional<EmitUnion<FunctionSignature<T>>>>
+export type EmitAsProps<T extends Function> = Props<MergeUnion<EmitUnion<FunctionSignature<T>>>>

If the original intent really was per-key optionality (e.g. [K]?:), then the implementation should be adjusted accordingly — please clarify.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/shared/useEmitAsProps.ts` around lines 88 - 90, The type
alias UnionToOptional is misnamed because it does not create optional
properties; either rename it to something like UnionToObject or
UnionToMergedProperties to reflect that it merges union members into a single
required-key object (update all usages of UnionToOptional accordingly), or if
the intent was to make per-key optionality, change the mapped type
implementation to produce optional properties (e.g., use [K]?: ...) and ensure
the value type includes undefined for missing branches; update references in
useEmitAsProps.ts to match the chosen approach (symbol: UnionToOptional) and
adjust tests/usages that rely on its semantics.

19-19: ⚡ Quick win

Replace bare Function type with explicit function signatures to satisfy no-unsafe-function-type lint rule.

The Function type is banned by @typescript-eslint/no-unsafe-function-type because it erases parameter and return typing. Since ToEmit<Name, Fn> guards against non-emit shapes, use an explicit signature like (...args: any[]) => any instead.

♻️ Suggested constraint changes
-export function useEmitAsProps<Name extends string, Fn extends Function = Function>(emit: ToEmit<Name, Fn>) {
+type AnyFn = (...args: any[]) => any
+export function useEmitAsProps<Name extends string, Fn extends AnyFn = AnyFn>(emit: ToEmit<Name, Fn>) {

Apply the same pattern to: useEmitAsProps.ts lines 38, 60–61, 84–86, 100, 105 and useForwardPropsEmits.ts lines 26, 31.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/shared/useEmitAsProps.ts` at line 19, The generic uses of
the banned bare Function type should be replaced with an explicit variadic
function signature to satisfy no-unsafe-function-type; update the generic Fn and
any explicit Function occurrences in useEmitAsProps (the export function
useEmitAsProps<Name extends string, Fn extends Function = Function> and the
other places noted around the file) to use Fn extends (...args: any[]) => any
(and default = (...args: any[]) => any) and replace other Function types inside
this file with (...args: any[]) => any; make the same replacements in
useForwardPropsEmits.ts for its function/generic declarations (the functions
referenced around lines 26 and 31) so all emit-related function types are typed
as (...args: any[]) => any instead of Function.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/shared/useEmitAsProps.ts`:
- Around line 94-98: The CamelCase type incorrectly lowercases the first segment
causing a mismatch with Vue's runtime camelize; update the CamelCase type (used
by HandlerKey) to preserve the original case of the leading segment instead of
applying Lowercase<T>, while still Capitalize-ing subsequent segments after '-'
so that names like 'My-event' map to 'MyEvent' and HandlerKey<TName> =
CamelCase<`on-${TName}`> yields the correct onMyEvent typing that matches Vue's
camelize behavior.
- Around line 38-45: The ToEmit conditional type currently infers a single
function signature (capturing only the final overload) which causes incorrect
narrowing for overloaded emit functions; change ToEmit to validate against the
full set of signatures produced by FunctionSignatureTuple (or
OverloadSignatureTuple) instead of using the single-signature infer, i.e.,
derive the Name/return-void check by iterating or mapping over
FunctionSignatureTuple<Fn> so every overload is considered; alternatively add
type-level regression tests (using expectTypeOf or a .tsd file) that assert
correct behavior for multi-overload emits to prevent regressions.

---

Nitpick comments:
In `@packages/core/src/shared/useEmitAsProps.ts`:
- Around line 55-58: Update the existing `@ts-expect-error` comment above the
`ExtendSignature` interface to include a concise rationale: state that extending
the generic type parameter `T` is intentional to inherit all overloads for the
recursive overload-walking algorithm, explain that removing the suppression
causes TypeScript to error and would break the recursion/bottoming-out logic
(silently changing overload inference), and add a note to preserve this
directive until TypeScript natively supports this pattern (or link to an
upstream issue/PR if available). Reference `ExtendSignature<T, TArgs, TReturn>`
and the recursive overload walk behavior in the comment so future maintainers
understand why the suppression must remain.
- Around line 88-90: The type alias UnionToOptional is misnamed because it does
not create optional properties; either rename it to something like UnionToObject
or UnionToMergedProperties to reflect that it merges union members into a single
required-key object (update all usages of UnionToOptional accordingly), or if
the intent was to make per-key optionality, change the mapped type
implementation to produce optional properties (e.g., use [K]?: ...) and ensure
the value type includes undefined for missing branches; update references in
useEmitAsProps.ts to match the chosen approach (symbol: UnionToOptional) and
adjust tests/usages that rely on its semantics.
- Line 19: The generic uses of the banned bare Function type should be replaced
with an explicit variadic function signature to satisfy no-unsafe-function-type;
update the generic Fn and any explicit Function occurrences in useEmitAsProps
(the export function useEmitAsProps<Name extends string, Fn extends Function =
Function> and the other places noted around the file) to use Fn extends
(...args: any[]) => any (and default = (...args: any[]) => any) and replace
other Function types inside this file with (...args: any[]) => any; make the
same replacements in useForwardPropsEmits.ts for its function/generic
declarations (the functions referenced around lines 26 and 31) so all
emit-related function types are typed as (...args: any[]) => any instead of
Function.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 27db19ab-89f3-443a-8513-32b6f3b0e969

📥 Commits

Reviewing files that changed from the base of the PR and between d2b2519 and 1d53b71.

📒 Files selected for processing (3)
  • packages/core/src/shared/useEmitAsProps.ts
  • packages/core/src/shared/useForwardProps.ts
  • packages/core/src/shared/useForwardPropsEmits.ts

Comment thread packages/core/src/shared/useEmitAsProps.ts Outdated
Comment thread packages/core/src/shared/useEmitAsProps.ts Outdated
@Myshkouski Myshkouski marked this pull request as draft May 14, 2026 22:32
The previous implementation directly pattern-matched on function signatures,
which only works for non-overloaded functions. This change utilizes
OverloadSignatureTuple to correctly extract and match overload signatures
against the expected emit name, enabling proper type inference for
functions with multiple overloads.
@Myshkouski Myshkouski marked this pull request as ready for review May 14, 2026 23:05
Expand the comment to clarify that ExtendSignature intentionally extends
generic type T to inherit all overloads for the recursive overload walking
algorithm, and warn against removing the suppression as it would break
the recursion/bottoming-out logic.
Rename the UnionToOptional type to MergeUnion to better reflect its
purpose of converting a union of function signatures into a single
merged object type with optional properties. The implementation remains
unchanged, only the type name is updated for improved clarity and
maintainability.
Replace the generic `Function` type with `AnyFn` from `@vueuse/shared` across
`useEmitAsProps` and `useForwardPropsEmits` utilities. This change improves type
inference and type safety by using a more specific function type constraint instead
of the overly broad `Function` type, which provides better IDE autocompletion and
catches more type errors at compile time. The runtime behavior remains unchanged.
@Myshkouski Myshkouski marked this pull request as draft May 15, 2026 16:43
Alexei Myshkouski added 2 commits May 15, 2026 20:14
- add package-private overload types from @vue/shared package
- simplify utility types
@Myshkouski Myshkouski marked this pull request as ready for review May 15, 2026 17:17
Copy link
Copy Markdown
Contributor

@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.

🧹 Nitpick comments (1)
packages/core/src/shared/useEmitAsProps.ts (1)

41-41: Emit type is currently internal; consider public re-export if it's needed for external consumers.

The Emit<Name, Fn> type is exported from useEmitAsProps.ts but not re-exported in the package index. Currently, it's used internally by useForwardPropsEmits.ts (imported directly from the source file) and no external imports were detected. This appears to be an intentional API boundary—only EmitAsProps and useEmitAsProps are exposed in the public API. If future use cases require external consumers to reference this type constraint when building wrappers or utilities, consider re-exporting it; otherwise, the current design keeps the constraint as an internal implementation detail.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/shared/useEmitAsProps.ts` at line 41, The review notes that
the Emit<Name, Fn> type is exported from useEmitAsProps.ts but not re-exported
in the package public API; decide whether to make it public—if you want
consumers to use the constraint, add a re-export for Emit from the package index
(alongside existing exports for EmitAsProps and useEmitAsProps) so external code
can import Emit; otherwise leave it internal and update references in
useForwardPropsEmits.ts to import from the internal file only. Ensure you
reference the Emit symbol and the related exports EmitAsProps and useEmitAsProps
when updating the package exports.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/core/src/shared/useEmitAsProps.ts`:
- Line 41: The review notes that the Emit<Name, Fn> type is exported from
useEmitAsProps.ts but not re-exported in the package public API; decide whether
to make it public—if you want consumers to use the constraint, add a re-export
for Emit from the package index (alongside existing exports for EmitAsProps and
useEmitAsProps) so external code can import Emit; otherwise leave it internal
and update references in useForwardPropsEmits.ts to import from the internal
file only. Ensure you reference the Emit symbol and the related exports
EmitAsProps and useEmitAsProps when updating the package exports.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bcbd0e0c-9f42-4bc8-ad00-f5de84e5e607

📥 Commits

Reviewing files that changed from the base of the PR and between 5204a77 and 4398849.

📒 Files selected for processing (3)
  • packages/core/src/shared/index.ts
  • packages/core/src/shared/useEmitAsProps.ts
  • packages/core/src/shared/useForwardPropsEmits.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/shared/useForwardPropsEmits.ts

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.

1 participant