Closed jods4 closed 1 month ago
As this is a type-only solution with no runtime change, there might be a way to apply this in user-land until Vue fixes this issue. It's slightly ugly, but it seems works.
[!WARNING] This is new and not battle-tested. Use with caution. That said, this work-around is only type trickery. JS code doesn't change and it cannot create bugs, only compilation errors.
The idea is simple: re-export the Vue component with an altered typing.
Let's say you have two components wrapper.vue
and inside.vue
. You'd like to pretend wrapper.vue
has props of inside.vue
:
You'll need to create a wrapper.ts
file that re-exports wrapper.vue
. It's the same component, but has different types.
Instead of importing wrapper.vue
, you'll import wrapper from "./wrapper.ts"
.
The typing is where this becomes quite ugly, as Vue types are very complex. I believe the solution below works for script setup components / types, it may need to be altered for Options API or functional components.
You'll need a way to extract Props types. Package vue-component-type-helpers has ComponentsProps<T>
. It's really short, you can as well copy it into your project if you prefer:
type ComponentProps<T> =
T extends new (...args: any) => { $props: infer P; } ? NonNullable<P> :
T extends (props: infer P, ...args: any) => any ? P :
{};
Then you'll need types to hide props from B
that are present in A
:
type Inherits<A, B> = A & Omit<B, keyof A>;
type FallthroughProps<A, B> = Inherits<ComponentProps<A>, ComponentProps<B>>;
Finally you can create a fake wrapper type.
This is the part that may only work if A
is a script setup component. Other magic types can probably be cooked for other components.
type HOC<A extends (...args: any) => any, B> = (props: FallthroughProps<A, B>, ...args: any) => ReturnType<A>;
And here's the example from before:
// File: wrapper.ts, import this instead of wrapper.vue
import Wrapper from "wrapper.vue"
import type Inside from "inside.vue"
export default Wrapper as HOC<typeof Wrapper, typeof Inside>
This even works when Wrapper
is generic, but genericity of Inside
and its potential link to Wrapper
is lost.
Typing can be written for related generic components, but I don't know how to write it without doing the expansion manually.
Here's an example with generic Inside<T>
and Wrapper<T>
, and the T
generic type should be the same based on Wrapper
template:
export const FallthroughWrapper = Wrapper as <T>(
props: FallthroughProps<typeof Wrapper<T>, typeof Inside<T>>,
...args: any
) => ReturnType<typeof Wrapper<T>>;
This pattern can be adapted to more generic arguments and relationships between Wrapper
and Inside
.
Well, I'm closing this issue as it seems I missed an update two months ago! https://github.com/vuejs/language-tools/pull/4103
It's sad it's an opt-in because of poor performance, as it's very useful, but it's better than nothing :) Hopefully the perf can be improved and become a default in the future!
I can't seem to get that setting to work, but that's another issue, though 😂
What problem does this feature solve?
Currently it's difficult to automatically strongly-type components that wrap other components (aka HOC). This is a common complaint, e.g. this RFC links to many issues: https://github.com/vuejs/rfcs/discussions/479
If a component
Out
wraps a componentIn
, the goal would be that tooling shows strong typing forIn
props onOut
, even thoughOut
doesn't declare them.What does the proposed solution look like?
My idea is not intrusive and would benefit everyone with zero code change.
SFC compiler and runtimes remain unchanged. They work fine and there's no need to modify them. What happens is that in
Out
, undeclared props end up inattrs
, and withinheritAttrs: true
they are copied toIn
props.Tooling is modified to change the declared props type of
Out
. Let's say components have props of typePO
andPI
respectively. When a component hasinheritAttrs: true
and a single root component in template, instead of declaringOut<PO>
, it's type is declared asOut<Inherit<PI, PO>>
.The secret sauce is a mapped type
Inherit
that copies properties that are not re-declared byPO
. Here's my take on it:These apparent props are a bit of lie, but the typing actually matches the runtime behaviour so I think it's a simple solution to a common issue.