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

Getting the PlayerId after a user subscribes in OneSignal with Ionic2

Currently working on an app with Ionic2 and facing a challenge with retrieving the player id after a user subscribes in order to store it in my database. Any suggestions on how I can retrieve the unique player id of OneSignal users post-subscription? ...

I'm having trouble asynchronously adding a row to a table using the @angular/material:table schematic

Having trouble asynchronously adding rows using the @angular/material:table schematic. Despite calling this.table.renderRows(), the new rows are not displayed correctly. The "works" part is added to the table, reflecting in the paginator, but the asynchron ...

Sending SMS from an Angular application to mobile devices can be achieved through several methods

Does anyone have experience sending SMS from an Angular6 web application? I would appreciate any guidance, such as reference links or code samples. Thank you! ...

What exactly does RouteComponentProps entail?

While exploring information on React, I came across the term RouteComponentProps. For example: import { RouteComponentProps } from 'react-router-dom'; const ~~~: React.FC<RouteComponentProps> and class BookingSiteOverview extends React.Com ...

Simple steps to turn off error highlighting for TypeScript code in Visual Studio Code

Hey there! I've encountered an issue with vscode where it highlights valid code in red when using the union operator '??' or optional chaining '?.'. The code still builds without errors, but vscode displays a hover error message st ...

A long error occurred while using the payloadaction feature of the Redux Toolkit

import { createSlice, PayloadAction, createAsyncThunk } from "@reduxjs/toolkit" import axios, { AxiosError} from "axios" type user = { id: number, token: string } export type error = { error: string } interface authState { user: user | ...

Error: It is not possible to assign a value to the Request property of the Object since it only has a getter method

Encountering issues while attempting to deploy my Typescript Next.js application on Vercel. The build process fails despite functioning correctly and building without errors locally. Uncertain about the root cause of the error or how to resolve it. The f ...

Substitute all instances of null bytes

I need to remove null bytes from a string. However, after replacing the null bytes \u0000 in the string let data = {"tet":HelloWorld.\u0000\u0000\u0000\u0000"} let test = JSON.parse(data).tet.replace("\u0000", ""); I always ...

Explore one of the elements within a tuple

Can we simplify mapping a tuple element in TypeScript? I'm seeking an elegant way to abstract the following task const arr: [string, string][] = [['a', 'b'], ['c', 'd'], ['e', 'f']] const f ...

Creating a type or interface within a class in TypeScript allows for encapsulation of

I have a situation where I am trying to optimize my code by defining a derivative type inside a generic class in TypeScript. The goal is to avoid writing the derivative type every time, but I keep running into an error. Here is the current version that is ...

How to specify the file path for importing a custom module?

I am currently learning Angular 2 and encountering an issue with importing a custom module that contains interface declarations. Here is my folder structure: https://i.stack.imgur.com/heIvn.png The goal is to import product.interface.ts into a component ...

Is there a way to enable Tail Recursion Optimization in TypeScript?

const isPositive = (n: number) => n > 0; function fitsIn(dividend: number, divisor: number, count: number, accum: number): number { if (accum + divisor > dividend) { return count; } return ...

What is the correct way to utilize default props in a Typescript-powered React component?

Recently diving into React, I find myself working on a basic child-component. My goal is to establish default props so that if no specific prop is provided when the component is invoked, it automatically resorts to the preset defaults. Here's what I&a ...

When in development mode, opt for the unminified version of the library in Web

My TypeScript project utilizes a forked version of the apexcharts npm package. When building the project with webpack in development mode, I want to use the unminified version of the apex charts library. However, for production, I prefer to stick with the ...

Is it possible to compile TypeScript modules directly into native code within the JavaScript data file?

I am seeking a way to break down an app in a TypeScript development environment into separate function files, where each file contains only one function. I want to achieve this using TS modules, but I do not want these modules to be imported at runtime in ...

What is the best way to assign the value of an HTTP GET request to a subarray in Angular 8

Attempting to store data in a sub-array (nested array) but despite receiving good response data, the values are not being pushed into the subarray. Instead, an empty array is returned. for (var j=0;j<this.imagesdataarray.length;j++){ this.http.g ...

The value 'var(--header-position)' cannot be assigned to type 'Position or undefined'

Description of Issue I am attempting to utilize a CSS Custom Property to customize a component within a nextjs application using TypeScript. Strangely, all CSS properties accept the CSS variables except for the position property which triggers the error b ...

Experimenting with Date Object in Jest using Typescript and i18next

I have included a localization library and within my component, there is a date object defined like this: getDate = () => { const { t } = this.props; return new Date().toLocaleString(t('locale.name'), { weekday: "long", ...

What is the best way to send props to a styled component without needing to convert them to transient props beforehand

Recently, I designed a custom Text component that accepts several props. These props are then forwarded to the styled component where specific styles are applied. However, I am facing an issue where I do not want these props to be passed down to the DOM, b ...

The render properties are not compatible with each other

I am currently using ReactQuill as a component, but I encounter this error when implementing it with Typescript. Do you have any suggestions on how to resolve this issue? The JSX element type 'ReactQuill' is not recognized as a constructor fun ...