vue-class-component: Can't use @Component on TypeScript abstract component class

I wrote an abstract common class that extends Vue where I want to put some common logic, including some hook methods like created(), but those methods are not invoked by the framework.

Here’s the abstract class implementing created().

abstract class DataEditorVue<T extends AbstractData> extends Vue {
  creation: boolean = false;
  data: T = null;

  created() {
    if (!this.data) {

      this.data = this.buildDataInstance();
    }
  }

  abstract buildDataInstance(): T;

  abstract getApiResource(): AbstractDataResource<T>;

  abstract getRouterPath(data: T): string;

  abstract getRouterPathAfterDeletion(data: T): string;

  remove() {
    return this.getApiResource().deleteFromObject(this.data).then(() => {
      return this.$router.push(this.getRouterPathAfterDeletion(this.data));
    });
  }

  create() {
    return this.getApiResource().create(this.data).then((data) => {
      return this.$router.push(this.getRouterPath(data), () => {
        this.creation = false;
        this.data = data;
      });
    });
  }

  update() {
    return this.getApiResource().update(this.data);
  }
}

Here the concrete class, extending abstract one.

@WithRender
@Component({
  components: {EditorControls}
})
class ObservationMilieuVue extends DataEditorVue<ObservationMilieu> {
  @Inject('observationTaxonResource')
  dataResource: ObservationTaxonResource;

  buildDataInstance(): ObservationMilieu {
    let data = new ObservationMilieu();
    data.localisation = {id: this.$route.params.localisationId};
    return data;
  }

  getApiResource(): ObservationTaxonResource {
    return this.dataResource;
  }

  getRouterPath(data: ObservationMilieu): string {
    return `/localisation/${data.localisation.id}/observation-milieu/${data.id}`;
  }

  getRouterPathAfterDeletion(data: ObservationMilieu): string {
    return `/localisation/${data.localisation.id}`;
  }
};

My actual workaround is to add a created() method in concrete class to invoke super.created() manually.

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Comments: 22 (6 by maintainers)

Most upvoted comments

That’s interesting. I did actually get abstract class mixins working fine and with type safety. Just had to hack around a bit with TypeScript. Not sure how stable this implementation is, but so far it seems to work fine.

Borrowing @lazarljubenovic example:

// note two things:
// 1) this is not the actually exported class, even though it contains all logic (If we'd export and use this as the actual mixin, nothing would work, since I guess abstract classes are only compile time and not runtime in TS?)
// 2) the @ts-ignore comment, which makes the @Component decorator stop complaining about the fact that it does not like abstract classes

// @ts-ignore
@Component
abstract class DiscardableFormMixinAbstract extends Vue {
  public abstract hasUnsavedChanges(): boolean
  public close() {
    if (this.hasUnsavedChanges) { 
      // open discard dialog and await for user's answer
    } else {
      // immediately close
    }
  }
}

// note two things again:
// 1) this is the actual export, which is an actual class than can be converted to JS
// 2) it again has @ts-ignore to make TypeScript stop complaining about the fact that it does not implement the abstract members of the base class

@Component()
// @ts-ignore
export class MessageModuleAuthorityStoreMixin extends MessageModuleAuthorityStoreMixinAbstract { }

then this will have compile error on the SomeForm class name until you implement the required abstract method: Non-abstract class 'SomeForm' does not implement inherited abstract member 'hasUnsavedChanges' from class 'DiscardableFormMixin'

@Component
export default class SomeForm extends mixins(DiscardableFormMixin) {
  mounted() {
    // this call is fine and works without problems
    this.close();
  }
}

Amazing! Thanks a lot @ReinisV ❤️Not sure how “stable” it is either, but it is indeed working as expected, and sounds like a well contained hack!

Seems like you simply forgot a @ts-ignore before the second @Component. Here is a fully functional example, enhanced for projects using eslint, in an example Login.ts file:

import { Component, Vue, PropSync, Emit } from 'vue-property-decorator';
import { Credentials, SourceGateway } from 'my-package';

// note two things:
// 1) this is not the actually exported class, even though it contains all logic (If we'd export and use this as the actual mixin, nothing would work, since I guess abstract classes are only compile time and not runtime in TS?)
// 2) the @ts-ignore comment, which makes the @Component decorator stop complaining about the fact that it does not like abstract classes

// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
@Component
abstract class Login extends Vue {
  public abstract setSourceGateway(): SourceGateway;

  @PropSync('credentials')
  syncedCredentials!: Credentials;

  @Emit()
  input() {
    return this.syncedCredentials;
  }

  mounted() {
    this.$nextTick(async () => {
      // we can rely on this function, which will have to be implemented
      this.setSourceGateway();
    });
  }
}

// note two things again:
// 1) this is the actual export, which is an actual class than can be converted to JS
// 2) it again has @ts-ignore to make TypeScript stop complaining about the fact that it does not implement the abstract members of the base class

/* eslint-disable @typescript-eslint/ban-ts-ignore */
// @ts-ignore
@Component
// @ts-ignore
export default class LoginMixin extends Login {}
/* eslint-enable @typescript-eslint/ban-ts-ignore */

And the implementation in an actual component, showcasing option merging to override the Prop default value:

import { Component, Prop, Emit, Mixins } from 'vue-property-decorator';
import { Credentials, MyGateway } from 'my-package';
import LoginMixin from '../abstracts/Login'; // or wherever you put it!

@Component({
  components: {
    Icon,
    GenericInput,
  },
})
export default class Login extends Mixins(LoginMixin) {
  // overriding the LoginMixin defaults for credential Prop
  @Prop({ default: 'a default value' }) credentials!: Credentials;

  // implementing abstract setSourceGateway: without it, VSCode
  // and TS will complain, which is exactly what we want here!
  @Emit()
  setSourceGateway() {
    return new MyGateway();
  }

  // from there, this logic is totally specific to your component
}

If I understand this thread correctly, it is not possible to use an abstract class on a mixin.

My use-case is that my mixin expects the component it’s used on implements a method. I am not sure how can I make this typesafe. What I’d love to do is this:

@Component
export abstract class DiscardableFormMixin extends Vue {
  public abstract hasUnsavedChanges(): boolean
  public close() {
    if (this.hasUnsavedChanges) { 
      // open discard dialog and await for user's answer
    } else {
      // immediately close
    }
  }
}

So, when I use this mixin on a component, that component needs to tell the mixin what it means that there are some unsaved changes. Currently I’m unable to do this.


What I tried doing was declaring an interface which the same name, which means that TS will merge the class and interface declaration into one:

export interface DiscardableFormMixin {
  hasUnsavedChanges: boolean
}

@Component
export class DiscardableFormMixin extends Vue {
  public close() {
    if (this.hasUnsavedChanges) { 
      // open discard dialog and await for user's answer
    } else {
      // immediately close
    }
  }
}

This allows me to use this.hasUnsavedChanges in the mixin, but when I use the mixin on a class, there is no error if I don’t specify the hasUnsavedChanges function.

@Component
export default class SomeForm extends mixins(DiscardableFormMixin) {
}

I need a behavior where this produces an error, telling me that I have to provide an implementation for the hasUnsavedChanges method.


What I realize I can do is declare the interface with a different name.

export interface DiscardableFormMixinInterface {
  hasUnsavedChanges: boolean
}

Then, when using the mixin:

@Component
export default class SomeForm extends mixins(DiscardableFormMixin) implements DiscardableFormMixinInterface {
}

The problem with this approach is (apart from having to re-define the type of this in the mixin, but that’s boilerplate I’m fine with) that I’m responsible for remembering to write the implements part. It should be an implicit thing.

To be clear, an abstract Mixin isn’t working?

Maybe you can consider extracts it as a single project.

Typed mixins are too useful for me to have to manually set up for each project, so I bit the bullet and extracted the code here into a module.

https://www.npmjs.com/package/vue-mixin-decorator

The last comment on this vue forum thread seems to have the current best answer; and it uses mixins -

https://forum.vuejs.org/t/how-can-i-use-mixin-with-vue-class-component-and-typescript/21254/8?u=chriszrc

@304NotModified you also can use mixin in this project .

/**
 * Created by jsons on 2017/5/27.
 */
import {Vue} from 'vue/types/vue'

import {ComponentOptions, FunctionalComponentOptions} from 'vue/types/options'

import {componentFactory} from "vue-class-component/lib/component";


function ComponentForMixin<V, U extends Vue>(options: ComponentOptions<U> | V): any {
    if (typeof options === 'function') {
        return componentFactory(options as any)
    }
    return function (Component: V) {
        return componentFactory(Component as any, options)
    }
}

type VClass<T extends Vue> = {
    new(): T
    extend(option: ComponentOptions<Vue> | FunctionalComponentOptions): typeof Vue
}

function Mixin<T extends Vue>(parent: typeof Vue, ...traits: (typeof Vue)[]): VClass<T> {
    return parent.extend({mixins: traits}) as any
}
export {ComponentForMixin, Mixin}

and use by this

import {Component, ComponentForMixin, Vue, Mixin} from "typings/base"


declare interface ApplePenTrait extends Pen, Apple, TestClass3 {
}
@Component
class Pen extends Vue {
    havePen() {
        alert('I have a pen')
    }
}
@Component
class Apple extends Vue {
    haveApple() {
        alert('I have an apple')
    }
}

@Component
class TestClass3 extends Vue {
    str3 = "TestClass3"
}

// compiles under TS2.2
@ComponentForMixin({
    template: `<span @click="Uh"> click show</span>`
})
export default class ApplePen extends Mixin<ApplePenTrait>(Apple, Pen, TestClass3) {
    havePen() {
        alert('I have a  pen (ApplePen)')
    }

    Uh() {
        this.havePen()
        this.haveApple()
        alert(this.str3)
    }
}

the idea is copy from @HerringtonDarkholme 's av-ts . thanks him !

I see ! Thanks for help, i’ll have a look to mixins.