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

declaration of beforeRouteEnter(to,from,next) hook should be updated #1863

Open
doommm opened this issue Nov 6, 2017 · 16 comments
Open

declaration of beforeRouteEnter(to,from,next) hook should be updated #1863

doommm opened this issue Nov 6, 2017 · 16 comments
Labels
Typescript Typescript related issues

Comments

@doommm
Copy link

doommm commented Nov 6, 2017

Version

3.0.1

Reproduction link

https://jsfiddle.net/doommm/50wL7mdz/74369/

Steps to reproduce

  • Vue 2.5.3
  • Vue-class-component 6.1.0
  • Typescript 2.6.1
  • vscode 1.17.2, vetur 0.11.0

I found that in declaration file(types/router.d.ts), there's a type named NavigationGuard used for beforeRouteEnter and other two hooks. The function declaration of next is ((vm: Vue) => any) | void) => void, and should it be updated so we could access all members declared in class(or object)?
I wrote three examples to explain my confusion.

  1. In .vue file with object literal style, i cant access my own properties.
  2. In .vue file with vue-class-component, if i add an type identifier manually, it works. no error. if not, error.
  3. In separate .ts file with vue-class-component, whether i add identifier or not, got error. But if i roll back to typescript 2.5.3, and add type identifier, it works.

I think the declaration should be updated(Generics? T extends Vue or ?), but i'm not sure about that. Sorry for my poor english If I dont describe the problem clearly.

What is expected?

all class(object) members should be accessed in vm.propertyName

What is actually happening?

We could only access members of Vue.

@posva
Copy link
Member

posva commented Nov 6, 2017

@ktsn Could you please give me a hand on this one whenever it's possible for you, please? 🙂

@ktsn
Copy link
Member

ktsn commented Nov 7, 2017

  1. In .vue file with object literal style, i cant access my own properties.

Since contextual ThisType of Vue.extend is only available in object literal, we cannot utilize it for vm's type of next callback. You need to manually annotate its type like below:

  beforeRouteEnter(to, from, next) {
    next((vm: { str: string } & Vue) => {
      vm.str = "sdf"
    });
  },

Or we could relax the vm's type as same as this type in mapState of Vuex vuejs/vuex#1044

  1. In separate .ts file with vue-class-component, whether i add identifier or not, got error. But if i roll back to typescript 2.5.3, and add type identifier, it works.

This is probably because of --strictFunctionTypes. https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-6.html
Disabling it would fix the error.

Maybe we should relax the type of vm to work with --strictFunctionTypes in any cases.

@HerringtonDarkholme What do you think?

@doommm
Copy link
Author

doommm commented Nov 8, 2017

@HerringtonDarkholme

Actually, i'm not quite understand. When we're coding a app class(or object?), we could ensure that the vm in the hook function is exactly what we want, because these hooks are coded for a specific class(object), isn't it?

Is it possible(or proper) to replace next with a generic one?
In types/vue.d.ts, ComponentOptions is declared like below now:

declare module "vue/types/options" {
  interface ComponentOptions<V extends Vue> {
    router?: VueRouter;
    beforeRouteEnter?: NavigationGuard;
    beforeRouteLeave?: NavigationGuard;
    beforeRouteUpdate?: NavigationGuard;
  }
}

There's an unused generic variable V, so could we just update the NavigationGuard to NavigationGuard<V> like below:

export type NavigationGuard<V extends Vue> = (
  to: Route,
  from: Route,
  next: (to?: RawLocation | false | ((vm: V) => any) | void) => void
) => any

At the same time, these two declarations could be updated like below:

export declare class VueRouter {
  // ...
  beforeEach (guard: NavigationGuard<Vue>): Function;
  beforeResolve (guard: NavigationGuard<Vue>): Function;
}
export interface RouteConfig {
 // ...
  beforeEnter?: NavigationGuard<Vue>;
}

I'm not sure about whether the type Vue should be used in these two places, and whether object literal would be benefited from it, do i miss something?

@doommm
Copy link
Author

doommm commented Nov 9, 2017

Consider this example. Blog is used to be only accessible via List component, so you annotate next as (vm: List) => any. Later, we add Blog links in Author's page. next typing is wrong then!

I'm still confused. There must be something i misunderstood or even never know.

I'm focusing on the In-Component guards beforeRouteEnter(to,from,next), and they(In-Component guards) should be tightly coupled with the component(or should be reusable?).

So for the example you posted, do you mean to define beforeRouteEnter in Blog? Shouldn't it become (vm: Blog) => any?

// Component List
@Component<List>({
  beforeRouteEnter(to, from, next) {
    next((vm: List) => {
      vm.blog; // accessible
    });
  },
})
class List extends Vue {
  blog: any;
}
// Component Author
@Component<Author>({
  beforeRouteEnter(to, from, next) {
    next((vm: Author) => {
      vm.blog;// accessible
    });
  },
})
class Author extends Vue {
  blog: any;
}
// Component Blog
@Component<Blog>({
  beforeRouteEnter(to, from, next) {
    next((vm: Blog) => {
      vm.p;// accessible
    });
  },
})
class Blog extends Vue {
  p: any;
}

@HerringtonDarkholme
Copy link
Member

HerringtonDarkholme commented Nov 9, 2017

@doommm I'm so sorry.

I mistaken the type of vm. It should be the component activated, not the previous one.

However, given this typing, I wonder if we can manage to give a good typing. Using generic isn't available here since V extends Vue won't accept specific type like Blog or so.

In classical OOP, such use case is covered by F-bounded polymorphism.

class Component<T extends Component<T>> {
   beforeRouteEnter(next: (vm: T) => any) {}
}

class Blog extends Component<Blog> {
  beforeRouteEnter(next: (vm: Blog) => any) {}
}

However, Vue isn't defined like this. None equivalent presents for object literal.

@HerringtonDarkholme
Copy link
Member

Hmm, fiddling with quantification might solve this

type NavigationGuard = (
  this: never,
  to: Route,
  from: Route,
  next: <V extends Vue>(to: (vm: V) => any) => void
) => any

  interface ComponentOptions<V extends Vue> {
    router?: VueRouter;
    beforeRouteEnter?: NavigationGuard;
    beforeRouteLeave?: NavigationGuard;
    beforeRouteUpdate?: NavigationGuard;
  }

Then

interface Test extends Vue {
    test: number
}
Vue.extend({
    beforeRouteEnter(to, from, next) {
        next((v: Test) => {

        })
    }
})

@HerringtonDarkholme
Copy link
Member

Note the type parameter quantification here. We make next itself as a generic type, so next((v: Test) => {}) actually is inferred per next call, not on definition site. This can decouple definition from implementation.

@uriannrima
Copy link

uriannrima commented May 24, 2018

I don't know if it gives any help while implementing a component with Vue.extend, but using vue-class-component I was in need of a type to "describe" the next function so to have my properties in "vm" reference, so I made this:

interface Next<T extends Vue = Vue> {
    (to?: (vm: T) => any): void
  }

This way, on beforeRouteEnter I do:

beforeRouteEnter(to: Route, from: Route, next: Next<Associa>) {
  next(vm => { 
    vm.providerMovies = []; /// <= Here I do have my properties defined.
  });
}

Where "Associa" (which is optional, it's Vue type if not passed) is my component as described below:

export default class Associa extends Vue

This way, I don't have any typescript errors and works pretty nicelly.

Btw: For those who are trying to use this way, you might do:

File shims-router.d.ts:

import Vue from 'vue';
declare module 'vue-router' {
  interface Next<T extends Vue = Vue> {
    (to?: (vm: T) => any): void
  }
}

Then:
import { Route, RawLocation, Next } from 'vue-router';

Again, so sorry if doesn't help in nothing, cause it helped me a lot.

@posva posva added the Typescript Typescript related issues label Jul 12, 2018
@posva
Copy link
Member

posva commented Jul 12, 2018

Anything we can do to improve current situation. I don't feel like including the helper type but it may be the right choice. Otherwise we could document this, we don't have a ts section in docs yet

@websmurf
Copy link

Not too sure if this is the right location for this question, but it is related to the discussion above.

I have a component annotated as following:

@Component({
  async beforeRouteEnter (to: Route, from: Route, next: any): Promise<any> {
    const { data } = await CustomerService.get(parseInt(to.params.id))
    next((vm: CustomerDetailsComponent) => {
        vm.customer = data
  }
})
export default class CustomerDetailsComponent extends Mixins(Acl, Translate) {
  customer: CustomerEntity = {
    // ... some properties
  }
}

This works fine and typescript accepts the vm.customer reference without issues.

If I apply a similar logic to the beforeRouteUpdate method, the this.customer reference won't be accepted:

  async beforeRouteUpdate (to: Route, from: Route, next: any): Promise<any> {
    const { data } = await CustomerService.get(parseInt(to.params.id))
    this.customer = data
    next()
  }

This results in an error:
Property 'customer' does not exist on type 'Vue'.

Is there a way to typehint the call so that typescript will get that the this reference refers to the CustomerDetailsComponent specifically and not a regular Vue instance?

I have tried to pass in the correct instance to the method, but this doesn't really work:

async beforeRouteUpdate<CustomerDetailsComponent>

What would be the correct way?

@gwardwell
Copy link

gwardwell commented May 14, 2019

@uriannrima's suggested solution did the trick for me. I think that solution, or one along the same lines, needs to be implemented to make life easier when developing Vue apps with strict: true set in tsconfig.json and the the @typescript-eslint/no-explit-any rule set.

@oleksii-shaposhnikov
Copy link

oleksii-shaposhnikov commented May 6, 2020

Just use the example:

import Vue from 'vue';
import { Component } from 'vue-property-decorator';
import { Route } from 'vue-router';

interface MyComponent extends Vue {
  route: Route;
}

@Component({
  beforeRouteEnter(to, from, next) {
    next(vm => {
      (vm as MyComponent).route = from;
    });
  }
})
export default class MyComponent extends Vue {
  private route!: Route;
}

@bianucci
Copy link

bianucci commented Sep 4, 2020

Would you be so kind as to provide an example where the composition api is used?
My typescript compiler is complaining because the type of vm has no parameter customNameParam.

Property 'customNameParam' does not exist on type 'Vue'.Vetur(2339)
export default defineComponent({
  beforeRouteEnter: (to: Route, from: Route, next: NavigationGuardNext) => {
    next(vm => {
      vm.customNameParam = from.name;
    });
  },
  setup() {
    const customNameParam = ref("");
    return {
      customNameParam,
    };
  },
});

@webrsb
Copy link

webrsb commented Jan 14, 2021

Try it

export default defineComponent({
  beforeRouteEnter: (to: Route, from: Route, next: NavigationGuardNext) => {
    next(vm: Vue & { customNameParam ?: string | null } => {
      vm.customNameParam = from.name;
    });
  },
  setup() {
    const customNameParam = ref("");
    return {
      customNameParam,
    };
  },
});

@websmurf
Copy link

Would you be so kind as to provide an example where the composition api is used?
My typescript compiler is complaining because the type of vm has no parameter customNameParam.

Same issue here, @webrsb solution unfortunately doesn't work since Vue is a namespace, not a type (at least in my setup with Vue 3). Have been messing around with ComponentPublicInstance, but can't get it to accept the addition property

@tinobino
Copy link

@websmurf, @webrsb Please try this:

import { ComponentPublicInstance, defineComponent, ref } from 'vue'
import { NavigationGuardNext, RouteLocationNormalized } from 'vue-router'

type CustomNameParamProvider = {
  customNameParam: string
} & ComponentPublicInstance

function isCustomNameParamProvider (vm: ComponentPublicInstance): vm is CustomNameParamProvider {
  return (vm as CustomNameParamProvider).customNameParam !== undefined
}

export default defineComponent({
  beforeRouteEnter: (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
    next(vm => {
      if (isCustomNameParamProvider(vm) && typeof from.name === 'string') {
        vm.customNameParam = from.name
      }
    })
  },
  setup () {
    const customNameParam = ref('')
    return {
      customNameParam
    }
  }
})

This is using an "user-defined type guard" for narrowing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Typescript Typescript related issues
Projects
None yet
Development

No branches or pull requests