Search code examples
typescripttypescript-genericstypescript-mixins

Overriding an abstract getter after a TypeScript Mixin


I have an app using some generic classes with the Mixin pattern, and am trying to implement a property getter that's the same for 90% of users of the mixin, but not quite all...

I was surprised to find:

  1. It's fine to define abstract methods, implement them in the mixin, and override them in a child class
  2. It's fine to define new property getters in the mixin and override them in the child class
  3. But, trying to define abstract property getters, implement them in the mixin, and override them in the child class - raises an error?

and found an unexpected issue when I tried to override a property getter defined in the mixin on a child class.

Below is a small code snippet that tries to demonstrate all 3 behaviours - can anybody help me understand why point #3 is an error and whether there's a good way to fix it? I'd like to keep the safety of any children of TextThing being forced to implement the abstract property...

/**
 * Basic interface objects should implement
 */
export interface ITextThing<T> {
  dataSource: T;
  html(): string;
  get text(): string;
}

/**
 * Abstract base class with partial implementation
 */
export abstract class TextThing<T> implements ITextThing<T> {
  dataSource: T;

  constructor(data: T) {
    this.dataSource = data;
  }

  abstract html(): string;
  abstract get text(): string;
}

type AbstractOrConcreteConstructor<T> = (new (...args: any[]) => T) | (abstract new (...args: any[]) => T);

/**
 * A mixin with extra functionality
 */
export function WithCounter<T extends AbstractOrConcreteConstructor<{}>>(SuperClass: T) {
  return class extends SuperClass {
    _counter: number;

    constructor(...args: any[]) {
      super(...args);
      this._counter = 0;
    }

    get count(): number {
      return this._counter;
    }

    get text(): string {
      return `[${this._counter}]`;
    }
  };
}

/**
 * The problem class: Using the mixin...
 */
export class TextWithCounter<TData> extends WithCounter(TextThing)<TData> {
  constructor(data: TData, initialCount = 0) {
    super(data);
    this._counter = initialCount;
  }

  // Overriding a getter defined in the mixin seems fine...
  override get count(): number {
    return this._counter + 1;
  }

  // Overriding a base class abstract function implemented in mixin seems fine...
  html(): string {
    return `<p><strong>${this.count}: </strong>${this.dataSource}</p>`;
  }

  // But base class abstract getter implemented in the mixin throws an error?
  //  > 'text' is defined as a property in class 'WithCounter<typeof TextThing>.(Anonymous class)
  //     & TextThing<TData>', but is overridden here in 'TextWithCounter<TData>' as an
  //     accessor.ts(2611)
  override get text(): string {
    return `[${this.count}]: ${this.dataSource}`;
  }
}

This project is currently using TypeScript v4.5.4, but could probably upgrade if it'd help? I can't quite tell if this is a bug/limitation or an issue with our code...


Solution

  • I think that this is cause by this bug. I think that getters are often treated as readonly properties which becomes an issue when inheriting like this. However, methods work properly, so a solution to this issue could be to replace declarations and definitions of get text() with getText(), then to add a concrete implementation of get text() on the abstract class which then calls getText(). So TextThing becomes:

    export abstract class TextThing<T> implements ITextThing<T> {
        dataSource: T;
    
        constructor(data: T) {
            this.dataSource = data;
        }
    
        abstract html(): string;
        abstract getText(): string;
        get text(): string {
            return this.getText();
        }
    }