Is it possible to create a prototype function within an interface that can group items in an array by a specific property, resulting in an array of objects containing a key and corresponding array of values?

I've been working on this code snippet and I'm trying to figure out how to make it work:

Array<T>.groupBy<KeyType> (property): {key: KeyType, array: Array<T> }[];

The code looks 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 encountered the following error at line 12 in '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 suspect that the issue lies within my KeyType definition, but I'm struggling to find a solution. KeyType is defined as a string but can also be a template literal type, which is what I currently require.

So, my questions are:

  • How can I resolve the issue and get the code to work?
  • Is there a way to incorporate T generics in the Array.prototype.groupBy function?
    • Currently, T is replaced with any because I'm unsure how to utilize T in the prototype definition.

Thank you in advance! :)

Answer №1

What steps can be taken to ensure that KeyType is a match?

The main issue at hand is that Object.entries currently has a hardcoded indexing value of string, rather than allowing for K extends PropertyKey (where PropertyKey represents all the types that can be used to index objects in JavaScript).

This problem can be resolved by incorporating an additional 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[] }[];
    }

    // The following code has been added - this addition creates an overload for `Object.entries`
    // which preserves the type of the key.
    interface ObjectConstructor {
      entries<K extends PropertyKey, V>(object: any): [K, V][];
    }
  }
}

Access the full code on the playground here

Is it possible to use T as a generic in the Array.prototype.groupBy function?

Absolutely, simply add a type parameter and specify the type of this to be of that specific type:

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

In addition, make sure to define the type of {} within your reduce call:

{} as {[X in KeyType]: T[]}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Angular findIndex troubleshooting: solutions and tips

INFORMATION = { code: 'no1', name: 'Room 1', room: { id: 'num1', class: 'school 1' } }; DATABASE = [{ code: 'no1', name: 'Room 1', room: { id: 'num1', ...

Ways to display the ping of a game server on your screen

Is there a way to display the game server's ping on the screen like in the example below? this.tfEnter.text = ShowPing + " ms"; Sometimes the code snippets provided in examples may not function properly. Channel List Image: https://i.stack ...

Guide on creating a detailed list of categories mapped to specific classes that all adhere to a common generic standard

Most TypeScript factory patterns I've encountered rely on a named mapping between a name and the Class type. A basic implementation example: const myMap = { classOne: ExampleClass, classTwo: AnotherClass } (k: string) => { return new myMap[k] } ...

What strategies can be employed to mitigate the activation of the losing arm in a Promise.race?

My current task involves sending the same query to multiple identical endpoints (about five) across various Kubernetes clusters. The goal is to aggregate the results without any delays and report failures to the user while continuing with the process seaml ...

Steps for assigning the TAB key functionality within a text area

Is there a way to capture the TAB key press within a text area, allowing for indentation of text when the user uses it? ...

"Exploring the world of Typescript's return statements and the

I'm currently grappling with a design dilemma in typescript. Within my controller, I perform a validation process that can either return a 422 response, which ends the thread, or a validated data object that needs to be utilized further. Here's a ...

Validating mixed types and generics within an array using Typescript's type checking

I am currently working with a setup that involves defining interfaces for animals and their noises, along with classes for specific animals like dogs and cats. I am also storing these animals in an array named pets. interface Animal<T> { name: stri ...

Are you harnessing the power of Ant Design's carousel next and previous pane methods with Typescript?

Currently, I have integrated Ant Design into my application as the design framework. One of the components it offers is the Carousel, which provides two methods for switching panes within the carousel. If you are interested in utilizing this feature using ...

Tips for monitoring changes to files while developing a NestJs application within a Docker container

Having an issue with NestJS and Docker here. Trying to run the development script using npm start: dev, but encountering a problem where the app runs fine but doesn't detect any changes in the source files, hindering the development process. Here&apo ...

Retrieve a specific attribute from a collection of JSON objects and transfer it to a separate object

Having a JSON object array with various project information: [ {"Project":"Project 1","Domain":"Domain1","Manager":"Manager1"}, {"Project":"Project 2","Domain":&q ...

Support for dark mode in Svelte with Typescript and TailwindCSS is now available

I'm currently working on a Svelte3 project and I'm struggling to enable DarkMode support with TailwindCSS. According to the documentation, it should be working locally? The project is pretty standard at the moment, with Tailwind, Typescript, and ...

Error TS2304: Unable to locate the word 'InputEvent'

While exploring the topic of whether TypeScript has type definitions for InputEvent, I experimented with using @types/dom-inputevent in my Angular 7 project. However, I kept encountering the error TS2304: Cannot find name 'InputEvent' whenever I ...

`Browser Extension Compatibility``

I am currently working on developing a TypeScript extension that is compatible with all major browsers. I have come across the package https://www.npmjs.com/package/web-ext-types which I have integrated into my package.json file. While coding in TypeScrip ...

Are there any alternatives to ui-ace specifically designed for Angular 2?

I am currently working on an Angular2 project and I'm looking to display my JSON data in an editor. Previously, while working with AngularJS, I was able to achieve this using ui-ace. Here is an example of how I did it: <textarea ui-ace="{ us ...

react Concealing the Card upon clicking a different location

Utilizing a combination of React and TypeScript, this component allows for the card to be toggled between shown and hidden states upon clicking on the specified div tag. However, there is a need to ensure that the card is hidden even if another area outs ...

Experiencing a compilation issue while attempting to apply the class-transformer

Encountering an issue while working with a basic example that involves class-transformer. error TS1240: Unable to resolve signature of property decorator when called as an expression. Argument of type 'ClassFieldDecoratorContext<Root, Project[]> ...

How can I compel npm to resolve dependencies flatly?

I am working on a project where multiple frontends share a common library. The module dependencies for these projects are managed using npm. In the package.json file of each project, I specify: "dependencies": { "mylib": "file:../<...path...> ...

Steer clear of including numerous variable values in Angular 2 while adjusting the class of selected items

Here's a question from someone who is new to Angular 2 and looking for an efficient way to change the active CSS class of tabs without using the router: activeTab: string; switchActiveTab(newTab: string) { this.activeTab = newTab; } <div clas ...

The assigned type does not match the type 'IntrinsicAttributes & { children?: ReactNode; }'. This property is not assignable

I have been struggling to resolve this issue, but unfortunately, I have not found a successful solution yet. The error message I am encountering is: Type '{ mailData: mailSendProps; }' is causing an issue as it is not compatible with type &apos ...

TypeORM - Establishing dual Foreign Keys within a single table that point to the identical Primary Key

Currently, I am working with TypeORM 0.3.10 on a project that uses Postgres. One issue I encountered is while trying to generate and execute a Migration using ts-node-commonjs. The problem arises when two Foreign Keys within the same table are referencing ...