#Easy way for an `Array` subclasses methods to return the subclass type.

10 messages · Page 1 of 1 (latest)

heavy axle
#

Is there a convenient way to create my own Array subclass where all of the various methods that return an array will return the subclass type instead?

Say I have a custom array type:

class MyArray extends Array {

    foo() {
        console.log("Pretend it's much more different from Array than this.")
    }
}

const bar = new MyArray();
const baz = bar.filter(n => n > 1);

bar.foo();
baz.foo(); // foo does not exist.  baz is any[] not MyArray

I want from and filter and map etc. to return MyArray not an Array. The only way I know how to do this is to tediously reimplement every method, where super.method is called and then the result is converted to MyArray and returned.

Is there a more convenient way to do this? Even a semi-convenient way for not having to re-describe every function signature + overloads.

verbal terrace
#

AFAIK, no there isn't really a convenient way to do this. Maybe slightly better than actually redeclaring all the methods at runtime is to just modify the type with declaration merging

#
class MyArray<T> extends Array<T> { foo!: string }

// utility type to reduce repetition on all the callback args
type MyArrayCalbackArgs<T> = [val: T, index: number, arr: MyArray<T>];
type MyArrayPredicateFunc<T> = (cb: (...args: MyArrayCalbackArgs<T>) => boolean) => MyArray<T>;

interface MyArray<T> {
  map<U>(fn: (...args: MyArrayCalbackArgs<T>) => U): MyArray<U>
  // would also want the type-guard case here, maybe
  filter: (cb: (...args: MyArrayCalbackArgs<T>) => boolean) => MyArray<T>;
  some: (cb: (...args: MyArrayCalbackArgs<T>) => boolean) => boolean;
  // special type-guard case
  every<U extends T>(cb: (val: T, index: number, arr: MyArray<T>) => val is U): this is MyArray<U>;
  every(cb: (...args: MyArrayCalbackArgs<T>) => boolean): boolean;
}
lavish pineBOT
verbal terrace
#

TBH, mostly I think subclassing built-ins like Array isn't very idiomatic in JS; a lot more common to just have a thing that has an array rather than making something that is an array.

#

And FWIW it's not right for TS to assume that array subclasses do return the subclass because this is actually controlled by the Symbol.species property

#
class Array1<T> extends Array<T> {}

class Array2<T> extends Array<T> {
  static [Symbol.species] = Array;
}

new Array1().map(x => x); // Array1
new Array2().map(x => x); // Array
#

... but the fact that it's been proposed is at least some evidence that this isn't considered particuarly idiomatic

heavy axle
#

Thanks for all of this. Very much appreciated.

I think "has an array" is probably the safer course.

My original motivation for wanting this is to have data views. The base structure representation is an array of elements, but the dozen types of them (GeoJSON Features), all have ids so I'd love a lazy implementation of myFeatures.asMapById and myFeatures.asObjectById which, because we're using copy-on-write, just become naturally invalidated that moment a new array is produced.

So the composition approach would be myCollection.asArray .asMapById etc.

The ergonomics feel good, but the implementation certainly has a growing smell.