I need to define a type Objs
which extends Array<Obj>
. I'd like to use it where a regular Array
is expected (for instance with the Array.prototype.*
functions) and I'd like to extend it with some functionalities specific for my Objs
type.
This is possible and very simple in JavaScript:
function id(val) { return val; }
class Objs extends Array {
objsMethod() {}
}
const objs1 = Objs.from([]);
console.log(objs1.constructor.name); // Objs
const objs2 = objs1.map(id);
console.log(objs2.constructor.name); // Objs
const objs3 = objs2.filter(id);
console.log(objs3.constructor.name); // Objs
However in TypeScript none of this works. Every function related to Array
returns an Obj[]
rather than an Objs
. Try for yourself:
function id<T>(val: T): T { return val; }
class Obj {}
class Objs extends Array<Obj> {
objsMethod() {}
}
const objs1 = Objs.from([]); // Obj[]
// ^?
const objs2 = (objs1 as unknown as Objs).map(id); // Obj[]
// ^?
const objs3 = (objs2 as Objs).filter(id); // Obj[]
// ^?
Is there any way to fix it without re-implementing the various functions?
For instance, can I forcefully override the type of Objs.from
, Objs.prototype.filter
etc without redefining them?
It's too bad that TypeScript doesn't have higher kinded types, so there's no way to automatically have subclasses of Array<T>
produce those subclasses from methods like map()
and filter()
and be properly generic. There's an open feature request at microsoft/TypeScript#10886 to allow Array
to inherit this way, but so far it's not part of the language.
So you do need to manually curate the types of the subclass methods. Your question is: can you do this purely at the type level without actually implementing them and emitting JavaScript for it?
The answer is yes: you can declare that a subclass's members have more specific types than those of the superclass, without having to reimplement them. That's what the declare
property modifier is for:
class Objs extends Array<Obj> {
declare static from: (arrayLike: ArrayLike<Obj>) => Objs;
declare map: (
(callbackfn: (value: Obj, index: number) => Obj) => Objs
) & Array<Obj>["map"];
declare filter: (
(predicate: (value: Obj, index: number) => unknown) => Objs
) & Array<Obj>["filter"];
objsMethod() { }
}
You can't currently use method syntax with declare
, see microsoft/TypeScript#38808, so that's why the above declarations look like function-valued properties instead of methods.
Also note how I needed to include the original Array<Obj>
method definitions as overloads (if you intersect function types, they behave like overloads). That's because we still need to support the original definitions of map
and filter
(e.g., if someone calls const o = objs1.map(x => 0)
you will get number[]
out instead of Objs
. You might wish to prohibit such a call, but you can't, because then an Objs
is not compatible with Array<Obj>
anymoe).
If you look at the emitted JavaScript for Objs
, it will have the objsMethod()
method but none of the type-overridden stuff:
// emitted js file
class Objs extends Array {
objsMethod() { }
}
Let's test it:
const objs1 = Objs.from([new Obj()]);
// ^? const objs1: Objs
const objs2 = objs1.map(id);
// ^? const objs2: Objs
const objs3 = objs2.filter(id);
// ^? const objs3: Objs
const o = objs1.map(x => 0)
// ^? const o: number[]
Looks good!