arraystypescriptoverridingself-reference

How to override only method signature in typescript?


A have an external array-like interface I want to implement

interface ListOf<T> {
  readonly length: number;
  readonly [index: number]: T;
  item(index: number): T | null;
  // note the parent parameter and its type
  forEach(callback: (value: T, key: number, parent: ListOf<T>) => void, thisArg?: any): void;
}

The interface looks very similar to normal array, so for implementation I choose to extend the Array class:

export class SimpleList<T> extends Array<T> implements ListOf<T> {
  constructor(arrayLength: number) {
    super(arrayLength);
  }
  item(index: number): T | null {
    return this[index] || null;
  }
}

But it fails

error TS2420: Class 'SimpleList' incorrectly implements interface 'ListOf'.
Types of property 'forEach' are incompatible.
...
Property 'item' is missing in type 'T[]' but required in type 'ListOf'

The error can easily be fixed by adding forEach with compatible signature:

  // class SimpleList<T>
  forEach(callback: (value: T, key: number, parent: SimpleList<T>) => void, thisArg?: any): void {
    super.forEach(callback, thisArg);
  }

But for me it looks really awkward - I just call super here, nothing changed at all, basically original method can do the same thing.

How can I tell typescript that original method with its signature actually matches the new one from the interface? I don't want an extra method that just re-declares one of superclass.

Beside the question I have a feeling that original Array should have declared its forEach as

forEach(callbackfn: (value: T, index: number, array: /*originally T[]*/ this) => void, thisArg?: any): void;

Not sure if its helpful but might lead to some solution.


Solution

  • It's technically unsafe to do what you're doing, at least according to Array's declaration. TypeScript thinks that the callback you give to forEach() might be called with a third argument of an arbitrary Array<T> instead of this. I don't know if TypeScript will ever change the Array method declarations to use this instead; there was a pull request at microsoft/TypeScript#46781 which has not been merged.

    Your workaround of

    forEach(callback: (value: T, key: number, parent: SimpleList<T>) => void, thisArg?: any): void {
      super.forEach(callback, thisArg); // error!
      //            ~~~~~~~~
    }
    

    also has an error in it, for the same reason. And, as you said, all it does is forward the call to super, which means it might as well not be overridden.

    The approach I'd take here instead is to explicitly narrow the type of SimpleList's forEach without touching the emitted JavaScript, by using a declare modifier. You can't use declare on methods (see microsoft/TypeScript#38008 and microsoft/TypeScript#48290 for relevant feature requests) so you can rewrite it as a function-valued property instead:

    class SimpleList<T> extends Array<T> implements ListOf<T> {
      constructor(arrayLength: number) {
        super(arrayLength);
      }
      item(index: number): T | null {
        return this[index] || null;
      }  
      declare forEach: 
        (callback: (value: T, key: number, parent: SimpleList<T>) => void, thisArg?: any) => void;
    }
    

    Playground link to code