Struggling with the TypeScript generic syntax for the GroupBy function

Struggling to figure out where I'm going wrong with this TypeScript signature after spending some time on it.

I've been working on a group by function:

const group = <T>(items: T[], fn: (item: T) => T[keyof T]) => {
  return items.reduce((prev, next) => {
    const prop = fn(next) as unknown as string;
    return {
      ...prev,
      [prop]: prev[prop] ? [...prev[prop], next] : [next],
    };
  }, {} as any);
};

group(data, (item) => item.type);

The current return type is:

const group: <Item>(items: Item[], fn: (item: Item) => string) => any

What I'm aiming for is:

const group: <Item>(items: Item[], fn: (item: Item) => string) => { admin: Item[], user: Item[] }

Here's the structure of the data:

interface User {
  type: string;
  name: string;
}

const data: User[] = [
  { type: 'admin', name: 'One' },
  { type: 'user', name: 'Two' },
  { type: 'admin', name: 'Three' },
];

I attempted this approach (with the object passed into reduce) but encountered an error and unsure about the solution:

{} as { [key: T[keyof T]: T[]] }

Check out the TS Playground link for the running code

Cheers!

Answer №1

Observation reveals that 'admin' and 'user' are solely derived from the data array's values (data[number]['type']).
TypeScript, by default, does not make assumptions beyond strings when it comes to values. (However, 'type' and 'name' are exceptions as they come from keys)

If you utilize as const for an array, TypeScript will impose more constraints on the values.
By limiting the valid values of User['type'], you can restrict it effectively:

let PossibleUserTypes = ['admin', 'user'] as const;  

interface User {
  type: typeof PossibleUserTypes[number];
  name: string;
}

const data: User[] = [
  { type: 'admin', name: 'One' },
  { type: 'user', name: 'Two' },
  { type: 'admin', name: 'Three' },
];

const group = <T extends User, U extends string>(items: T[], fn: (item: T) => U) => {
  return items.reduce((prev, next) => {
    const prop = fn(next)
    return {
      ...prev,
      [prop]: prev[prop] ? [...prev[prop], next] : [next],
    };
  }, {} as {[x in U] : T[] } );
};
let temp = group(data, (item) => item.type);
console.log(temp);
/*
inferred typing: 
let temp: {
    admin: User[];
    user: User[];
}
*/

Eliminating the 'as const' will result in the output being { [x: string]: User[];}, without any restrictions.

Note: Instead of using

type: typeof PossibleUserTypes[number];
, you can directly use type: 'admin' | 'user'; the difference lies in no longer accepting just any string like in the original code.

Another option is to use {} as Record<U,T[]> where temp will be interpreted as

Record<'user'|'admin', User[]></code allowing code completion from <code>temp.
to temp.admin and temp.user. Removing as const removes these restrictions.

One might consider applying as const to the data array instead of PossibleUserTypes, but the resulting type would be highly complex (you can explore this yourself):

const data  = [
  { type: 'admin', name: 'One' },
  { type: 'user', name: 'Two' },
  { type: 'admin', name: 'Three' },
] as const 

const group = <T, U extends string>(items: Readonly<T[]>, fn: (item: T) => U) => {
  return items.reduce((prev, next) => {
    const prop = fn(next)
    return {
      ...prev,
      [prop]: prev[prop] ? [...prev[prop], next] : [next],
    };
  }, {} as {[x in U] : T[] } );
};
let temp = group(data, (item) => item.type);
console.log(temp)

Answer №2

The function signature does not specify the return type of the function. To define the return type as T, you can modify it like this:

const group = <T>(items: T[], fn: (item: T) => T[keyof T]):T => { return items.reduce((prev, next) => {
const prop = fn(next) as unknown as string;
return {
  ...prev,
  [prop]: prev[prop] ? [...prev[prop], next] : [next],
};}, {} as any);};
console.log(group<Item>(data, (item) => item.type));

Try running this code in the Typescript playground here

I made some changes to the code based on your comment. You can view the updated version here. The return type must be a static type.

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

Having difficulty locating the correct TypeScript interface for executing GraphQL queries in a React application using Apollo Client

In this React component code snippet, a table is returned with each row containing data fetched from a backend API using GraphQL. While the data is successfully fetched, there seems to be an issue in defining the correct interface for handling the data ret ...

Tips for converting API data to DTO (Data Transfer Object) using TypeScript

Here is an array of vehicles with their details. export const fetchDataFromApi = () => { return [ { vehicleId: 1, vehicleType: 'car', seats: 4, wheelType: 'summer', updatedAt: new Date().toISOString }, { vehicleId: 2, vehic ...

Showing the child component as undefined in the view

Within my Angular application, I encountered an issue involving a parent component named DepotSelectionComponent and its child component SiteDetailsComponent. The problem arises when an event called moreDetails is emitted to the parent component, triggerin ...

Tips for Logging HTTP Communication Errors in Angular

When making an HTTP put call to update a record in my .Net MVC application, I have noticed that the controller's put logic is not being triggered as expected compared to other types of HTTP requests. I want to implement error handling by using the Ha ...

What is the proper way to utilize variables in package.json with npm version 7.x.x?

I am looking to utilize npm scripts to access keys found in the directories section. "directories": { "client": "client", "server": "server" }, "scripts": { "test:client&qu ...

Verify the anticipated URL and showcase the real URL

During a functional test, I am attempting to create a "Then" step where the current URL is verified. After researching on SO, it appears that the proper approach is to wait for the URL to match the expected one: Then('The URL contains {string}' ...

Angular-template static functions refer to functions that do not require an

Our project utilizes the linting-config provided by AirBnB. There is a rule that stipulates class methods must utilize this or be declared as static. While this rule theoretically makes sense, it seems to present challenges within an angular context. Consi ...

In production mode, ExpressJs dispatches the stack efficiently

Before going live, I want to test production simulation with the following setup: package.json "start": "cross-env NODE_ENV=production node dist/index.js", index.ts console.log(process.env.NODE_ENV) // prints "production" ro ...

What are the essential requirements for an Angular application to function properly with minimal dependencies?

If a new developer wants to begin learning Angular from scratch, what are the essential npm packages that they need to install in order to start building an Angular application with minimal dependencies? ...

Access an Angular 2 component through an email hyperlink including querystring parameters

I need to create a deep link with query string parameters for a component, so that when the link is clicked, it opens up the component in the browser. For example: exmaple.com/MyComponent?Id=10 I want to include a link in an email that will open the com ...

Create a new data structure in TypeScript that stores multiple values in a

For my TypeScript project, I came across a situation where I needed to utilize Promise.all(...) to handle an array of multiple items: Promise.all( firstRequest, secondRequest, ..., nthRequest ) .then((array : [FirstType, SecondType, ..., NthType]) ...

NodeJS can be used to convert JSON data into an XLSX file format and allow for

I am currently working on a project in nodejs where I need to convert JSON data into XLSX format and then download it to the client's browser. I have been using the XLSX npm module to successfully convert the JSON data into a Workbook, however, I am f ...

TypeORM find query is returning a data type that does not match the defined entity type

In my infrastructure module, I am using the code snippet below: import { Student } from "core" import { Repository } from "./Repository" import { Database } from "../../db" export class UserRepository<Student> extends Re ...

Troubleshooting the NullInjectorError in Angular - Service Provider Missing?

I'm facing an issue in my code where I have buttons that should trigger pop-ups displaying details as a list when clicked. However, every time I click the buttons, I encounter the error mentioned below. It seems like I am unable to access the desired ...

Is Babel necessary for enabling JavaScript compatibility in my TypeScript React project, excluding Create React App?

This is the webpack configuration for my React project built in TypeScript, module.exports = { mode: 'development', entry: ['./src/main.tsx'], module: { rules: [ { // Rule for ts/tsx files only, no rule for js/js ...

Extending a Typescript class from another file

I have a total of three classes spread across three separate .ts files - ClassA, ClassB, and ClassC. Firstly, in the initial file (file a.ts), I have: //file a.ts class ClassA { } The second file contains: //file b.ts export class ClassB extends Class ...

Tips for transferring information between two components when a button is clicked in Angular 2

I am currently working on a code that displays a table on the main page with two buttons, "Edit" and "Delete", for each row. When the Edit button is clicked, a modal opens up. My question is, how can I pass the "employee id" of a specific employee to the ...

A step-by-step guide on bundling a TypeScript Language Server Extensions LSP using WebPack

Currently, I am working on a language server extension for vs-code that is based on the lsp-sample code. You can find the code here: https://github.com/microsoft/vscode-extension-samples/tree/master/lsp-sample My challenge lies in WebPacking the extension ...

Share information by including the provider within the @component declaration in Angular

I am looking to explore a new method of passing data using providers directly within the component itself. For instance, I am curious if there is a way to pass a variable from one component to another without relying on routing or other traditional methods ...

A Vue component library devoid of bundled dependencies or the need for compiling SCSS files

My current challenge involves the task of finding a way to publish our team's component library. These components are intended to be used by various internal applications within our organization. I have specific requirements: The library must be acc ...