0

so I'm trying to make this work: Array<T>.groupBy<KeyType> (property): {key: KeyType, array: Array<T> }[]; The code look like this :

type ArrayByParameter<T, KeyType = any> = string | ((item: T) => KeyType);
declare global {
  interface Array<T> {
    groupBy<KeyType = string>(
      property: ArrayByParameter<T,KeyType>
    ): { key: KeyType; array: T[] }[];
  }
}
if (!Array.prototype.groupBy) {
  Array.prototype.groupBy = function <KeyType = string>(
    property: ArrayByParameter<any, KeyType>
  ) {
    let callbackFunction: (item: any) => any;
    if (typeof property === "string") {
      callbackFunction = (mapObj) => mapObj[property];
    } else if (typeof property === "function") {
      callbackFunction = property;
    } else {
      throw "Parameter is not a string nor a function!";
    }
    // Edit version of : https://stackoverflow.com/a/34890276/3781156
    return Object.entries(
      this.reduce(function (rv, x) {
        (rv[callbackFunction(x)] = rv[callbackFunction(x)] || []).push(
          x
        );
        return rv;
      }, {}) as { [key: string]: Array<any> }
    ).map(([key, array]) => {
      return { key, array };
    });
  };
}
type Characters = "A" | "B" | "C";
type Numbers = "1" | "2" | "3";
type SomeKeyType = `${Characters}-${Numbers}`;
// Same thing as below.
type SomeKeyType2 =
  | "A-1"
  | "A-2"
  | "A-3"
  | "B-1"
  | "B-2"
  | "B-3"
  | "C-1"
  | "C-2"
  | "C-3";
type SomeObject = {
  c: Characters;
  n: Numbers;
  // ...
};
const array: SomeObject[] = [
  { c: "A", n: 1 },
  { c: "A", n: 2 },
  { c: "A", n: 2 },
  { c: "B", n: 1 },
  // ...
];
const groupByArray: { key: SomeKeyType; array: SomeObject[] }[] =
  array.groupBy<KeyType>((obj: SomeObject) => `${obj.c}-${obj.n}`);
Result expected :
[
  {key: "A-1", array: [{c:A, n:1, /*...*/}]},
  {key:"A-2", array: [{/*...*/},{/*...*/}]},
  {key:"B-1", array:[{/*...*/}]},
  /*...*/
];

I'm getting error like this at line 12 at "Array.prototype.groupBy" :

Type '<KeyType = string>(property: ArrayByParameter<any, KeyType>) => { key: string; array: any[]; }[]' is not assignable to type '<KeyType = string>(property: ArrayByParameter<any, KeyType>) => { key: KeyType; array: any[]; }[]'. Type '{ key: string; array: any[]; }[]' is not assignable to type '{ key: KeyType; array: any[]; }[]'. Type '{ key: string; array: any[]; }' is not assignable to type '{ key: KeyType; array: any[]; }'. Types of property 'key' are incompatible. Type 'string' is not assignable to type 'KeyType'. 'KeyType' could be instantiated with an arbitrary type which could be unrelated to 'string'.ts(2322)

I think my KeyType definition is the issue, but I am not able to find a solution. KeyType is a string, but could also be a template literal types which is what I want right now.

So :

  • How could I fix the issue so the code work ?
  • Is there a way to get T generics in Array.prototype.groupBy function ?
    • Right now, T is replace with any, because I don't know how to use T in the prototype definition.

Thanks in advance ! :)

Maxime4000
  • 77
  • 2
  • 11

1 Answers1

1

How could I fix the issue so that KeyType matches?

The key issue here is that Object.entries is currently hard-coding the key indexer to string instead of allowing K extends PropertyKey (PropertyKey is a built-in type alias for all the types one can index into objects with in JavaScript).

Nothing that we can't fix with an extra ambient type definition:

declare module "our-global-ambient-module" {
  global {
    interface Array<T> {
      groupBy<KeyType extends PropertyKey = string>(
        property: ArrayByParameter<T, KeyType>
      ): { key: KeyType; array: T[] }[];
    }

    // What we've added - an overload to `Object.entries`
    // that will preserve the key's type.
    interface ObjectConstructor {
      entries<K extends PropertyKey, V>(object: any): [K, V][];
    }
  }
}

Full code on the playground here

Is there a way to get T as a generic in Array.prototype.groupBy function

Yes, just add a type parameter and type this to be of that type:

Array.prototype.groupBy = function <T, KeyType extends PropertyKey = string>(
  this: T[],
  property: ArrayByParameter<any, KeyType>
) 

You'll also need to annotate the type of {} in your reduce call:

{} as {[X in KeyType]: T[]}
Sean Vieira
  • 155,703
  • 32
  • 311
  • 293