volna
volna

Reputation: 2610

How to create a util for the class

What I have done
My initial approach is to use the following mixin that returns a class decorator:

function createMixin (behaviour: any, sharedBehaviour: any = {}): any {
  const instanceKeys: any = Reflect.ownKeys(behaviour)
  const sharedKeys: any = Reflect.ownKeys(sharedBehaviour)
  const typeTag = Symbol(`isa`)

  function _mixin (clazz: Function): Function {
    for (let property of instanceKeys) {
      Object.defineProperty(clazz.prototype, property, {
        value: behaviour[property],
        writable: true
      })
    }

    Object.defineProperty(clazz.prototype, typeTag, { value: true })

    return clazz
  }

  for (let property of sharedKeys) {
    Object.defineProperty(_mixin, property, {
      value: sharedBehaviour[property],
      enumerable: sharedBehaviour.propertyIsEnumerable(property)
    })
  }

  Object.defineProperty(_mixin, Symbol.hasInstance, {
    value: (i: any) => !!i[typeTag]
  })

  return _mixin
}

export const customUtil = createMixin({
  customUtil (event: any) {
    console.log(this)
  }
})

So later the util can be used to decorate the class and can access this of the target class without any problem:

import { customUtil } from 'utils'

@customUtil
export default class ComponentClass extends Vue {
  someClassMethod() {
    this.customUtil() // successfully outputs the whole class in the console
  }
}

But it results in the tslinter warning TS2339: Property 'customUtil' does not exist on type 'ComponentClass'.

Questions
1. Is there a possibility to resolve the linter issue by somehow "typing" the method assigned to the class by the mixin utility
2. If there is an alternative approach to have utility functions/classes that will have no issue to access this of the class they are used in and with a simple way of attaching them?

Upvotes: 0

Views: 339

Answers (1)

pascalpuetz
pascalpuetz

Reputation: 5418

This issue has been discussed in other Threads and is also a pending issue in the TypeScript github. But there are two ways to get around this.

1: Typecasting

You could just typecast this to either forget the class Context or enhance it with the nescessary information.

import { customUtil } from 'utils'

@customUtil
export default class ComponentClass extends Vue {
  someClassMethod() {
    (this as any).customUtil(); // Now the linter will be fine but you will lose type safety
    (this as any as {customUtil: (event:any) => void}).customUtil(); // This works and you could/should extract the type. 
  }
}

But as you can see, this is not ideal.

2: Real TypeScript Mixins

You could use real TypeScript Mixins instead of Decorators:

Utils

type Constructor<T = {}> = new (...args: any[]) => T;

// The Mixin
export function WithUtils<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    customUtil (event?: any) { // Actually, 'event' needs to be optional
      console.log(this)
    }
  };
}

// A Base class so you can just extend if needed
export const UtilsBase = WithUtils(class {});

Component

export default class ComponentClass extends WithUtils(Vue) {
  someClassMethod() {
    this.customUtil() // successfully outputs the whole class in the console
  }
}

Class not extending from Vue

export default class SomeClass extends UtilsBase {
  someClassMethod() {
    this.customUtil() // successfully outputs the whole class in the console
  }
}

// alternatively, you can use the Mixin with an unnamed class
export default class SomeClass extends WithUtils(class {}) {
  someClassMethod() {
    this.customUtil() // successfully outputs the whole class in the console
  }
}

Upvotes: 1

Related Questions