VLAZ suggested using ArrayElements.from()
, which should work at runtime:
In ES2015, the class syntax allows sub-classing of both built-in and user-defined classes. As a result, static methods such as Array.from()
are "inherited" by subclasses of Array
, and create new instances of the subclass, not Array
.
Source: MDN
However, TypeScript's type system has no way to express this, so the typings of Array.from()
are set to return T[]
instead of a specific subclass.
You could work around this by overriding the from
static method:
class ArrayElements<T> extends Array<T> {
static from<T>(iterable: Iterable<T> | ArrayLike<T>): ArrayElements<T>
static from<T, U>(
iterable: Iterable<T> | ArrayLike<T>,
mapFn: (v: T, k: number) => U,
thisArg?: unknown
): ArrayElements<U>
static from<T, U = T>(
...args: [
Iterable<T> | ArrayLike<T>,
((v: T, k: number) => U)?,
unknown?
]
): ArrayElements<U> {
return super.from(...args as Parameters<typeof Array.from>) as ArrayElements<U>;
}
// ...
}
Alternatively, you could use a type assertion for the class constructor to avoid adding something at runtime:
class _ArrayElements<T> extends Array<T> {
// ...
}
type ArrayElements<T> = _ArrayElements<T>
const ArrayElements = _ArrayElements as unknown as Omit<typeof _ArrayElements, 'from'> & {
from<T>(iterable: Iterable<T> | ArrayLike<T>): ArrayElements<T>
from<T, U>(
iterable: Iterable<T> | ArrayLike<T>,
mapFn: (v: T, k: number) => U,
thisArg?: unknown
): ArrayElements<U>
}
Also, here's a way to type sum
safely:
// https://stackoverflow.com/a/54520829/8289918
type KeysMatching<T, V> = {[K in keyof T]: T[K] extends V ? K : never}[keyof T]
class ArrayElements<T> extends Array<T> {
// The T extends object ? ... : never prevents this being called
// for ArrayElements of primitives
sum(key: T extends object ? KeysMatching<T, number | undefined> : never): number {
return this.reduce((a, b) => a + (b[key] as unknown as number | undefined || 0), 0);
}
}
// errors
ArrayElements.from(['']).sum() // string is not an object
ArrayElements.from([{a: ''}]).sum('a') // value of a is not number
// ok
ArrayElements.from(array).sum('aaa')
Playground link