Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clarify V3 Migration strategy for TS Vue.extend (#2) #1502

Closed
lukashroch opened this issue Feb 8, 2022 · 17 comments
Closed

Clarify V3 Migration strategy for TS Vue.extend (#2) #1502

lukashroch opened this issue Feb 8, 2022 · 17 comments

Comments

@lukashroch
Copy link

Sorry for re-opening, but how does this apply for single components & vue3? This is OK for augmenting the global vue (which is already in vue2), but not for per-component case in vue3. The original example is for vue2 & single component.

https://vuejs.org/api/utility-types.html#componentcustomproperties

Originally posted by @yyx990803 in #1446 (comment)

@leopiccionia
Copy link
Contributor

leopiccionia commented Feb 9, 2022

I don't know the position of Vue team, but, as a user, if I aimed for optimal TypeScript support:

  • instead of local mixins, I'd use the Composition API — it was originally created for composition-over-inheritance scenarios like this (see comparison);
  • instead of this.$refs, I'd use Composition API's typed template refs for HTML elements and Vue components.

@lukashroch
Copy link
Author

I get that you can use for all of that composition API. We indeed want to use that in future. But for large codebases, that's not an easy path to rewrite suddenly everything in one go using composition API.

Especially if it was supported in Vue2 and as I understand Vue3 still fully supports Options API. So I'm hoping Typescript support for Options API can also kept in place. Otherwise it juts got crippled for TS users ... keeping Options API in place with removed TS support. So vue2 TS users will be forced to rewrite all at once or nothing.

@leopiccionia
Copy link
Contributor

You don't need to "rewrite suddenly everything in one go".

Composition API can be used inside Options API.

You can migrate mixin by mixin, consumer by consumer, often without having to touch the rest of the component (if the mixins didn't have implicit dependencies in non-mixin code).

But, yes, I don't know if there's an easy and type-safe migration path for TypeScript users that want to keep using local mixins.

@lukashroch
Copy link
Author

Let's assume vue2 is swapped to vue3 and we deal with local mixins in local components (!no global vue instance augmentation!).

  • you have large code base written in vue2 & TS, so most of the components are probably using Vue.extend
  • syntax export default (Vue as VueConstructor<Vue & InstanceType<typeof someMixin & whateverOtherTypesThatAreNotResolvedAutomatically...>).extend({...} is gone -> no longer in V3.
  • so you swap to defineComponent, which resolves setup() but not mixins etc...
  • your typings from Vue.extend are gone
  • Options API still works for JS ... but not for TS anymore

Am I missing something how you can do this gradually without rewriting everything in one go if TS relied on Vue.extend? Happy to be guided here, really :-)

@leopiccionia
Copy link
Contributor

Anyway, after further research, it seems that defineComponent should correctly infer types in mixins also defined with defineComponent (see PR). I've never used mixins in Vue 3 + TS code, so I don't know if this assumption holds in real world.

The full suite of defineComponent tsc checks, including support for mixins and extends options:
https://github.com/vuejs/core/blob/main/test-dts/defineComponent.test-d.tsx

@lukashroch
Copy link
Author

Resolves to unknown for me, even if all defined with defineComponent, but that's only just the mixins...

@LinusBorg
Copy link
Member

LinusBorg commented Feb 9, 2022

So let's approach this gradually and identify how far we can get:

~~Concerning a gradual approach, you could use the Vue 2 composition API plugin to be able to use defineComponent() in Vue 2 already, making it possible to switch your components over one by one before moving on to Vue 3.~~~

Edit: Unfortunately, the plugin's version of defineComponent doesn't offer the same support of options API as Vue 3 core's, I'm afraid.

  1. Mixins do indeed work with defineComponent():

Bildschirmfoto 2022-02-09 um 19 40 20

  1. $refs should be not too common throughout a codebase (unless it relies on tons of jquery stuff, and if so, can be typecast in place without too much hassle. Another alternative would be to use template refs with setup as a gradual migration strategy:
export default defineComponent({
  setup() {
    const someComponent = ref<typeof SomeComponent>()

    return {
      someComponent
    }
  },
  // rest of the component can keep using Options API, 
  // replacing `this.$refs.someComponent` with `this.someComponent`
})
  1. The only sticking point for now would be Vuex. It's use is likely common throughout your codebase and notoriously hard to type. I don't see a nice way to migrate that off the top of ,y head. To be honest, pretty much all Vue 2+TS codebases that use Vuex which I came across used some sort of 3rd party wrapper. Your approach wasn't even on our, or at least my, radar.

@lukashroch
Copy link
Author

lukashroch commented Feb 9, 2022

Thanks, already using composition plugin and trying to have new work vue3 forward compatible. Though these are approaches how to do the rewrites to composition API, not to keep types for existing Options API which is still supported, but weirdly types are not.

So once vue2 to vue3 is swapped, Vue.extend used for TS is gone, Options API is still there and types gone. Nothing gradual about that. So by that time you want to swap, you have to have everything rewritten or stick with vue2 + composition plugin and also lagging with other 3rd party upgrades.

I was more wondering if they could be provided through defineComponent generics or something similar.

@LinusBorg
Copy link
Member

@pikax do you have any bright ideas on how we could enable that?

@pikax
Copy link
Member

pikax commented Feb 9, 2022

@pikax do you have any bright ideas on how we could enable that?

@LinusBorg maybe providing an Vue.extend on @vue/compat, basically defineComponent but with a different name.


Vue3 provides superior typescript support than vue2, with full support for Options API.

@lukashroch do you mind providing a concrete example of what is not supported? Composition-API is preferred instead of Mixins going forward.

Before I misunderstand your question do you mind explaining (ideally with a repo code/link) what you cannot achieve in vue3 but you're able to do it on vue2?


  • you have large code base written in vue2 & TS, so most of the components are probably using Vue.extend

Personally I would find&replace, replacing Vue.extend with defineComponent. Easiest solution, you could make it more complex, but I don't think is worth it.

  • syntax export default (Vue as VueConstructor<Vue & InstanceType<typeof someMixin & whateverOtherTypesThatAreNotResolvedAutomatically...>).extend({...} is gone -> no longer in V3.

Do you mind explaining a bit better? That is something that I would try to avoid, if you need mixins, just import them in the component.

defineComponent({
 mixins: [ someMixin ]
 // ....
})
  • so you swap to defineComponent, which resolves setup() but not mixins etc...

Not sure what you mean here, if the mixins are not working, that's a bug and should be fixed, can you provide an example?

  • your typings from Vue.extend are gone

They are replaced with the defineComponent and Utility Types

  • Options API still works for JS ... but not for TS anymore

This should work in vue3 for sure! Can you provide an example?

@LinusBorg
Copy link
Member

@pikax let me try and give you a quick recap of the problem:

As we know in Vue 2 Options API, there are a bunch of things that are not straightforward to get type inference for when usign Vue.extend() which is the default approach to get some level oft ype hints. Among these are:

  • properties from mixins
  • $refs
  • Properties from Vuex

OP found a way that works for them by extending the VueConstructor<> type in the way they shared above, essentially typecasting those properties that were not inferrable /annotatable by Vue automatically onto the current components this .

But that way is not replicable in Vue 3's defineComponent() as there is no such generic to extend, so they are worried that a migration to Vue 3 would mean that they had to fundamentally rewrite all their components at once in composition API or something as otherwise all these types break.

So the main question is: is there a way to extend an individual component's this type (public instance) with arbitrary interfaces similar to what OP did? And extending from that, is there a way to achieve that with defineComponent() in the Vue 2 plugin, which in my quick experiment at least, was not able to infer mixin types the way Vue 3's was able to?

@pikax
Copy link
Member

pikax commented Feb 10, 2022

Thank you @LinusBorg

Now I understand the issue that OP is facing.

Extending this type on defineComponent is not possible (at the least without major hacks), this is because we already override the this on the defineComponent argument, Vue3 does not support extend API (only with compat, but there's no typings there).

  • properties from mixins
  • $refs
  • Properties from Vuex
  • properties from mixins this should work when using mixin or extend, no type override should be required.

  • $refs not supported to define $refs, simply we cannot support this because there's no way to declare it, also the advantages this will bring are not many, the recommended way to do this is by using setup which will be type safe - types(runtime-core): support typed safe vnode ref on the template core#5387

  • Properties from Vuex this is hit-and-miss, if using pure vuex (no third-party libs), you can achieve a similar behaviour by casting your map* functions

declare function mapGetters(o: any): any

defineComponent({
  computed: {
    // NOTE we need to cast it to be a function, just like a computed is a function
    ...(mapGetters(['foo', 'bar']) as { foo: ()=> number; bar: ()=>string })
  },

  methods: {
    test(){
      this.foo.toExponential()

      // @ts-expect-error
      this.bar = this.foo
    }
  }
})

@lukashroch
Copy link
Author

@pikax, I'm not questioning superiority of the vue3 & ts support. It works like a charm in vue3. And I agree this Vue.extend syntax is not great, but that's just how it was vue2 & vuex etc... I'd like to get rid of it, but for large code bases it will take time. So if we could we do this gradually, that would be great. But I will totally understand if you say ... can't do ... You have to migrate. At the moment I'm trying assess if it's possible or not so we're not chasing something which is not there.

The issue is that Options API & TS works with defineComponent, but only if you convert everything. I don't think you can mix & match defineComponent and Vue.extend with injected types like in the example. So you got to jump the boat, find and replace everything with defineComponent...

So if you have such interface, and various types with different migration strategies, you have to resolve them at once, since you removed the whole thing. And if you have codebase with hundreds of components and mixins, entangled, you suddenly have to migrate large chunk of code at once.

In that interface you can e.g. have:

  • mixins - OK those will resolve automatically
  • $refs - relatively easy to do if you don't have too much logic around them, then you start to rewiring the component itself
  • local callbacks in created, like debounce or something... again, can convert with setup, but if logic is in data() etc, you start to rewiring the component and rewriting to composition API
  • mixin merging strategy issues - data() and setup() are resolved in different time and setup() does not have this. So if mixins / components rely on merging strategy and you move one mixin to setup() you break it since setup() resolve it before data and later mixin overwrites
  • and then there is vuex bindings ... which will be a challenge itself

Each of these can be resolved and will be better once it's done in composition API, I'm not arguing about that. But the problem is that if you take the Vue.extend out, you have resolve these all at once.

@pikax
Copy link
Member

pikax commented Feb 10, 2022

The issue is that Options API & TS works with defineComponent, but only if you convert everything. I don't think you can mix & match defineComponent and Vue.extend with injected types like in the example. So you got to jump the boat, find and replace everything with defineComponent...

That's correct, they won't work together well. My recommendation is moving everything to defineComponent, it might be tedious and a lot of work, but supporting a direct cast is not reasonable IMO.

If you migrating, the first thing is to check the migration, after you are using vue3 (without typechecking yet), the next step is to replace the Vue.extend with defineComponent, I would not add or changing javascript code on those components, only tweaking Typescript, the first few components will be tricky to figure out how to do it, but you will find a pattern within your codebase.

  • $refs - relatively easy to do if you don't have too much logic around them, then you start to rewiring the component itself

If having typed $refs is a must, I would cast them inline or using a computed. I would not add a setup just for that.

  • local callbacks in created, like debounce or something... again, can convert with setup, but if logic is in data() etc, you start to rewiring the component and rewriting to composition API

Lifecycle are a bit tricker to alter, but you can do something like this (I don't recommend that, but it might be an option), there's no need to use setup at all in most of the cases.

  • mixin merging strategy issues - data() and setup() are resolved in different time and setup() does not have this. So if mixins / components rely on merging strategy and you move one mixin to setup() you break it since setup() resolve it before data and later mixin overwrites

I guess you won't have this issue if you don't port to setup on phase1, you can gradually deprecate mixins while you converting them to composition API (if that's the goal)

Each of these can be resolved and will be better once it's done in composition API, I'm not arguing about that. But the problem is that if you take the Vue.extend out, you have resolve these all at once.

Direct casting always comes back to bit you, I can provide help on particular scenarios and how to possibly type them correctly, each codebase is different.

@lukashroch
Copy link
Author

@pikax, @LinusBorg thank you, really helpful to have these answers!

Does the TS inference work for defineComponent in composition plugin or only in vue3? I already use composition plugin for newer code, but not yet with mixins inference. So just did a quick test and it resolves to unknown. Am I missing something?
Screenshot 2022-02-10 134411

@pikax
Copy link
Member

pikax commented Feb 10, 2022

@lukashroch yes only on Vue3

@lukashroch
Copy link
Author

@pikax thanks for confirming!

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

No branches or pull requests

4 participants