Organize TypeScript objects by specific properties to create precise key-value associations

In need of a group by function that accurately models group types. For example, it should be clear that tag.value.format() should undergo type checking since we are operating within the release group.

const tags: ParsedTag[] = [
  { type: 'release', value: Semver.parse('1.2.3')! },
  { type: 'unknown', value: 'foobar' },  
]

const tagGroups = groupByProp(tags, 'type')

// Objective: Ensure .format passes type checks
console.log(tagGroups.release.map(tag => tag.value.format()))

My attempts to construct such a function are documented in this particular TS Playground demo.

Answer №1

I was able to find a solution after some trial and error. Here is the complete code:

type IndexableKeyTypes = string | number | symbol

type Indexable<T = unknown> = Record<string | number, T>

type JustIndexableTypes<T> = T extends IndexableKeyTypes ? T : never

type KeysMatching<Rec, Keys> = NonNullable<
  {
    [RecKey in keyof Rec]: Rec[RecKey] extends Keys ? RecKey : never
  }[keyof Rec]
>

type GroupBy<T extends Indexable, K extends IndexableKeys<T>> = {
  [KV in JustIndexableTypes<T[K]>]: Array<T extends Record<K, KV> ? T : never>
}

type IndexableKeys<Rec> = KeysMatching<Rec, IndexableKeyTypes>

export function groupByProp<Obj extends Indexable, KeyName extends IndexableKeys<Obj>>(
  xs: Obj[],
  keyName: KeyName
): GroupBy<Obj, KeyName> {
  type KeyValue = JustIndexableTypes<Obj[KeyName]>
  const seed = {} as GroupBy<Obj, KeyName>

  return xs.reduce((groupings, x) => {
    const groupName = x[keyName] as KeyValue

    if (groupings[groupName] === undefined) {
      groupings[groupName] = []
    }

    groupings[groupName].push(
      x as Obj extends Record<KeyName, KeyValue> ? Obj : never
    )

    return groupings
  }, seed)
}

//
// TEST
//

import * as Semver from 'semver'

type ParsedTag =
  | { type: 'unknown'; value: string }
  | { type: 'release'; value: Semver.SemVer }

const tags: ParsedTag[] = [
  { type: 'release', value: Semver.parse('1.2.3')! },
  { type: 'unknown', value: 'foobar' },  
]

const tagGroups = groupByProp(tags, 'type')

 // Goal: .format type checks
console.log(tagGroups.release.map(tag => tag.value.format()))

You can access a TS Playground instance of this code here.

The part that initially posed a challenge for me was:

type GroupBy<T extends Indexable, K extends IndexableKeys<T>> = {
  [KV in JustIndexableTypes<T[K]>]: Array<T extends Record<K, KV> ? T : never>
}

I eventually realized that by iterating over the keyed-value types, I could apply a filter expression on T.

This solution hinges on having a discriminate property within the union, which I found to be quite reasonable and intuitive.

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

"Encountering an issue with TypeScript test files being blocked while utilizing the karma-webpack

I have been encountering an issue while trying to utilize karma-webpack for compiling my typescript tests in conjunction with karma. Lately, my tests have ceased running. When inspecting the developer console, I come across messages like the following, fo ...

I am struggling to understand the significance of the $ symbol in this particular context

I came across the following snippet in a book I've been reading: `images/${Date.now()}.jpg` The curly brackets used here signify 'out of string', but I'm unsure about the meaning of $... P.S. Honestly, I didn't want to ask a que ...

What is the correct way to trigger an event specified as a string parameter in the emit() function?

My current goal is to pass the emit name as a string (for example, 'showComponent') from child to parent. I then want to trigger another emit in the emitAction(callbackName: string) function, and finally execute the showComponent() function. I&a ...

Guide on loading a PDF asset dynamically within an Angular application with the help of webpack

I am having trouble loading a PDF file into my Angular app, which is running on the webpack dev server. I am using the HTML <object> tag with the data attribute to achieve this. The issue arises because the PDF path is generated dynamically at runti ...

Material UI autocomplete is not detecting the options parameter as an array

I am currently working on implementing an autocomplete field that retrieves options from my component's state, which in turn fetches data from the backend. Here is a snippet of my component: export const Person: React.FC<PersonProps> = ({name, a ...

What is the best way to incorporate an array of strings as a value while updating a state object?

I have written some code with the intention of concatenating two strings later on. However, I am struggling to update the state properly using a handleChange function. The error message I'm receiving states that "A spread argument must either have a t ...

TypeScript: additional information on the properties of an interface

Attempting to develop a framework that enables meta on interface fields. Consider the following example: function path(path) {} interface ITwoProps { @path('some/path') stringProp: string; @path('different/path') boolProp: boolean ...

An error message occurs in TypeScript when trying to access a property that does not exist in an array

I'm having trouble figuring out this issue... I am receiving data from an API and storing it as an array. I'm attempting to access the data in this format... this.data.results[i].datas[j].dataType However, I keep getting the error "property res ...

What causes the "Error: method not allowed" message to appear when attempting to send a "DELETE" request from a Next Js component? (The POST method is

This is my first time on this platform, and I'm currently following a tutorial from Javascript Mastery to create a clone of a thread application. After watching the entire video and building the basic functionality based on it, I decided to enhance th ...

Issue with the createActionThunk feature in redux toolkit

Hey there, I'm having an issue with my createAsyncThunk function. It seems to be working fine in manipulating the state by removing a value with a specific index, but for some reason, it's not updating the Firebase database. Also, when I try to a ...

The script type `(close: any) => Element` cannot be assigned to type `ReactNode`

Encountering a problem while adding a popup in my TypeScript code Error: Type '(close: any) => Element' is not compatible with type 'ReactNode'. <Popup trigger={ <button className="fixed bott ...

Utilizing Typescript and RequireJS for Incorporating jqueryui

Recently, I've been encountering issues with getting jQueryUI to function properly. Strangely enough, prior to attempting to integrate jQueryUI, using jQuery alone worked perfectly fine. The current problem I'm facing is receiving a "TypeError: ...

Removing unnecessary files from a frontend npm package in a production environment: Best practices

Having trouble organizing the build process for my frontend web app created with Angular 2 and TypeScript. This is the structure I'm working with: / - dist/ <-- transpiled .js files - src/ <-- .ts files - assets/ - bower_components/ ...

Are there any Android applications specifically designed for editing Typescript files?

While this may not be a typical programming inquiry, I have phrased it in a binary manner in hopes of meeting the criteria. I have been utilizing Quoda on my Android device and have encountered the need to edit .ts / Typescript files, which the app does n ...

The npm build command is triggering an error message that reads "Allocation failed due to ineffective mark-compacts near heap limit."

I'm currently working on a ReactJS/TypeScript project on Windows 10. I've been running into issues when trying to build my TypeScript scripts using the following command: "rimraf ../wwwroot/* && react-scripts-ts build && copyfi ...

Is there a way to effectively eliminate an array of objects in JavaScript or TypeScript and alter the object structure simultaneously?

I am seeking solutions to restructure an object that has multiple arrays of objects so that I can access the object directly. console.log(value.data.summary[0].data) Instead of accessing arrays in this manner, I want to modify my data structure. Is there ...

How to Verify User Login Status with Firebase Auth in Node.js Cloud Functions

My goal is to create an Express dynamic web page on Node.js. I aim to execute the following logic on the server (Firebase Cloud Functions) at path /: If the client is currently logged in (Firebase Auth), the home page my_home_page.html should be rendered ...

Testing the Snackbar function with Angular and TypeScript: Unit Testing

I've encountered an issue with a method called onDelete that utilizes a MatSnackBar which is injected in the constructor like so: constructor(private todoListService: TodolistService, private snackBar: MatSnackBar) { } onDelete(todoList: TodoList): v ...

I'm sorry, but it seems that the property 'propName' cannot be found in type '{}'

I've been working on creating a Typescript function that returns an object, but I keep encountering the following error message: ERROR in src\app\components\model\model.component.html(3,30): : Property 'heading' does not ...

Mastering the proper implementation of observables, async/await, and subscribing in Angular

I have a JSON file located at assets/constants/props.json. Inside this file, there is a key called someValue with the value of abc. The structure of the JSON file can be seen in the following image: https://i.stack.imgur.com/MBOP4.jpg I also have a serv ...