feat(core): enhance type inference for useEmitAsProps and useForwardPropsEmits#2643
feat(core): enhance type inference for useEmitAsProps and useForwardPropsEmits#2643Myshkouski wants to merge 11 commits into
Conversation
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.
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughStronger TypeScript typing for emit- and prop-forwarding: ChangesEmit + Props forwarding type system
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
packages/core/src/shared/useEmitAsProps.ts (3)
55-58: 💤 Low value
@ts-expect-erroronExtendSignaturedeserves 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 winRename
UnionToOptional— it doesn't introduce optionality.The mapped type collects keys across all union members and emits required properties (with
neverfilling 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 winReplace bare
Functiontype with explicit function signatures to satisfyno-unsafe-function-typelint rule.The
Functiontype is banned by@typescript-eslint/no-unsafe-function-typebecause it erases parameter and return typing. SinceToEmit<Name, Fn>guards against non-emit shapes, use an explicit signature like(...args: any[]) => anyinstead.♻️ 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.tslines 38, 60–61, 84–86, 100, 105 anduseForwardPropsEmits.tslines 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
📒 Files selected for processing (3)
packages/core/src/shared/useEmitAsProps.tspackages/core/src/shared/useForwardProps.tspackages/core/src/shared/useForwardPropsEmits.ts
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.
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.
- add package-private overload types from @vue/shared package - simplify utility types
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/core/src/shared/useEmitAsProps.ts (1)
41-41:Emittype is currently internal; consider public re-export if it's needed for external consumers.The
Emit<Name, Fn>type is exported fromuseEmitAsProps.tsbut not re-exported in the package index. Currently, it's used internally byuseForwardPropsEmits.ts(imported directly from the source file) and no external imports were detected. This appears to be an intentional API boundary—onlyEmitAsPropsanduseEmitAsPropsare 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
📒 Files selected for processing (3)
packages/core/src/shared/index.tspackages/core/src/shared/useEmitAsProps.tspackages/core/src/shared/useForwardPropsEmits.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/core/src/shared/useForwardPropsEmits.ts
🔗 Linked issue
#2642
❓ Type of change
📚 Description
This PR enhances TypeScript type inference for the
useEmitAsPropsanduseForwardPropsEmitscomposition API utilities:useEmitAsPropsnow properly types the returned props based on the emit function signature:Fnfor the emit function typeToEmit,EmitAsProps,FunctionSignature, etc.) for handling function overloadsuseForwardPropsEmitsreceives improved type inference:ComputedRefreturn types based on whether emit is providedWithOptionalBooleanstype utility for external useChanges:
packages/core/src/shared/useEmitAsProps.ts- Core type improvements, 81 lines added/modifiedpackages/core/src/shared/useForwardProps.ts- ExportWithOptionalBooleanspackages/core/src/shared/useForwardPropsEmits.ts- Add function overloads for proper type inferenceThis 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 updated the documentation accordingly.(public API hasn't changed)Summary by CodeRabbit