Vue-router: declaration of beforeRouteEnter(to,from,next) hook should be updated

Created on 6 Nov 2017  路  14Comments  路  Source: vuejs/vue-router

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.

Typescript

Most helpful comment

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.

All 14 comments

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

  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 https://github.com/vuejs/vuex/pull/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?

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

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;
}

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

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) => {

        })
    }
})

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.

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.

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

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?

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

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;
}

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,
    };
  },
});

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,
    };
  },
});
Was this page helpful?
0 / 5 - 0 ratings

Related issues

kerlor picture kerlor  路  3Comments

gil0mendes picture gil0mendes  路  3Comments

posva picture posva  路  3Comments

cnweibo picture cnweibo  路  3Comments

shinygang picture shinygang  路  3Comments