-1

I am trying to apply this accepted answer to add a given property of an array of objects. However, I'm getting different errors, depending on the situation:

Case 1:

var array = [{aaa: 123}, {aaa: 1313}];

class ArrayElements extends Array {
  sum: (key: string) => number = (key: string) => {
    return this.reduce((a, b) => a + (b[key] || 0), 0);
  };
}

const value = (array as ArrayElements).sum("aaa");
console.log(value);

In that case I get the execution error array.sum is not a function.

I thought that's because I didn't follow exactly the same way to create the object. However, if I do

Case 2:

const values = new ArrayElements(...[{aaa: 123}, {aaa: 1313}]);

then I get a compilation error Argument of type '{ aaa: number; }' is not assignable to parameter of type 'number'..

I am using TypeScript, not plain JavaScript, but I guess that shouldn't be the problem...?

xavier
  • 1,860
  • 4
  • 18
  • 46
  • 3
    `as` doesn't actually cast the value. It just tells the type checker what it should assume the value is. – deceze Aug 23 '21 at 14:12
  • 4
    Type *assertions* (when you use `as`) are not type *castings*. They don't change the object, they just tell the compiler to treat it *as if* it was a different object. If I hand you a piece of cake tell you it's tea, it won't suddenly become liquid. – VLAZ Aug 23 '21 at 14:12
  • Ok, I see it, of course. O_O Thank you very much for the answers, I spent 2 hours looking for the error. Now I just need to know how to do the actual cast... :-D Any easy way or I have to do an ugly map? – xavier Aug 23 '21 at 14:20
  • @VLAZ tempting everyone with dessert-based analogies again, I see. – Andy Aug 23 '21 at 14:24
  • 1
    @Andy I food is the first thing that came to mind. I'm a bit ill and undernourished, so I apologise. – VLAZ Aug 23 '21 at 14:26
  • 3
    @VLAZ hope you feel better soon. – Andy Aug 23 '21 at 14:27
  • @xavier `ArrayElements.from(array)`, perhaps. – VLAZ Aug 23 '21 at 14:27
  • @VLAZ thank you for the answer. Sadly it doesn't work: `Property 'sum' does not exist on type '{ aaa: number; }[]'.`. arggggggg :-( – xavier Aug 23 '21 at 14:30
  • Case 2 works in deno. No typing errors there. Maybe make the type of array you are extending from more specific like class ArrayElements extends Array<{[key: string]: number}> {} – Ali Baykal Aug 23 '21 at 16:35

1 Answers1

0

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

Lauren Yim
  • 12,700
  • 2
  • 32
  • 59