I think there is no need to specify any static
properties, as it would require implementing all of them.
If you plan on overriding certain methods from Array.prototype
, it's better to only type those specific methods that you are interested in.
import { Expect, Equal } from '@type-challenges/utils';
class MyArray<T> extends Array<T> {
override map<U>(callbackfn: (value: T, index: number, array: MyArray<T>) => U, thisArg?: any): MyArray<U> {
return []
}
}
const instance = new MyArray<number>()
const result = instance.map((elem) => elem * 2)
type test = Expect<Equal<typeof result, MyArray<number>>> // ok
Playground
The issue here is that Symbol.species
is a static property and cannot be accessed from an instance.
Refer to the source code for more information.
Take a look at the following example:
type Keys = {
[P in keyof SetConstructor]:P
}
// type Keys = {
// readonly prototype: "prototype";
// readonly [Symbol.species]: typeof Symbol.species;
// }
And:
type Keys = {
[P in keyof Set<number>]:P
}
// type Keys = {
// add: "add";
// clear: "clear";
// delete: "delete";
// forEach: "forEach";
// has: "has";
// readonly size: "size";
// entries: "entries";
// keys: "keys";
// values: "values";
// [Symbol.iterator]: typeof Symbol.iterator;
// readonly [Symbol.toStringTag]: typeof Symbol.toStringTag;
// }
Therefore, Symbol.species
can only be inferred from the constructor. What does this mean in practice?
const NOT_IMPLEMENTED = null as any;
type ClassType = new (...args: any[]) => any
// if Symbol.species] exists in T - it is a COnstructor
type Species<T> = T extends {
readonly [Symbol.species]: infer Constructor;
} ? Constructor : never
interface MyArray<T> {
[Symbol.iterator](): IterableIterator<T>;
}
interface MyArrayConstructor {
new(): any
[Symbol.species]: MyArrayConstructor
}
declare var MyArray: MyArrayConstructor;
const builder = <T extends ClassType>(constructor_: T): Species<T> => NOT_IMPLEMENTED
const set = builder(Set) // SetConstructor
const array = builder(Array) // ArrayConstructor
const myArray = builder(MyArray) // MyArrayConstructor
type Test1 = Species<typeof MyArray> // MyArrayConstructor
type Test2 = Species<typeof Set> // SetConstructor
type Test3 = Species<typeof Map> // MapConstructor
Consider this example:
type GetPrototype<T> = T extends { prototype: infer Proto } ? Proto : never
const NOT_IMPLEMENTED = null as any;
type Species<T> = T extends {
readonly [Symbol.species]: infer Constructor;
} ? Constructor : never
type IteratorElement<T extends Iterable<any>> = T extends {
[Symbol.iterator](): Iterator<infer Elem>
} ? Elem : never
type Instance = InstanceType<SetConstructor> // Set<unknown>
function map<Elem extends Iterable<any>, Constructor extends Species<Elem> >(array: Elem, cb: (elem: IteratorElement<Elem>) => any): GetPrototype<Elem> {
return NOT_IMPLEMENTED;
}
// GetPrototype<Elem> === never
// Constructor
const result = map(new Set<number>(), elem => elem + 2)
It is feasible to infer the instance of a constructor but not the other way around when dealing with just an instance.