Advanced type generics in Typescript

I've hit a roadblock in my attempt to ensure type safety for a function that should take an object and return a number. Despite numerous efforts, I haven't been successful. To give you a simple example (the actual application involves more complex interfaces), consider the following:

interface Parent {
  id: number;
  children: Child[];
}

interface Child {
  text: string;
}

const parents: Parent[] = [
  {
    id: 1, 
    children: [
      {text: 'Child 1a'}, {text: 'Child 1b'}, 
      {text: 'Child 1c'}, {text: 'Child 1d'}, 
      {text: 'Child 1e'}
    ]
  },
  {
    id: 2, 
    children: [
      {text: 'Child 2a'}, {text: 'Child 2b'}, {text: 'Child 2c'}
    ]
  }  
];

function getMaxNumChildren<T>(data: T[], childKey: keyof T) {
  return data.reduce((max: number, parent: T) => {
    return max > parent[childKey].length ? max : parent[childKey].length;
  }, 0);
}

console.log(getMaxNumChildren<Parent>(parents, 'children')); // 5

As expected, parent[childKey].length throws an error since TypeScript is unaware that T[keyof T] represents an array.

I've experimented with casting to any[], among other approaches, but have yet to find a solution while keeping the function fully generic. Any suggestions?

Answer №1

To make this function work in the simplest way possible, consider making it generic in K, which represents the type of childKey. Annotate that the data is an array of objects with keys in K and properties containing a numeric length property as shown below:

function getMaxNumChildren<K extends keyof any>(
  data: Array<Record<K, { length: number }>>,
  childKey: K
) {
  return data.reduce((max, parent) => {
    return max > parent[childKey].length ? max : parent[childKey].length;
  }, 0);
}

By following this structure, the compiler can verify that parent[childkey] has a numeric length without errors. You can then call the function like this:

console.log(getMaxNumChildren(parents, 'children')); // 5

It's important to note that you no longer need to call

getMaxNumChildren<Parent>(...)
, as the generic type is now based on the key type rather than the object type. You could use
getMaxNumChildren<"children">(...)
if desired, but letting type inference handle it usually works well.


Hopefully, this solution fits your needs. If not, feel free to provide more details by editing the question. Best of luck!

Link to code

Answer №2

To inform TypeScript of two generic types, both a key and an object need to be defined. The first type represents a key, while the second type pertains to an object where that key corresponds to an array value.

Consider implementing the following code snippet:

function getMaxNumChildren<TKey extends string, TObj extends { [key in TKey]: unknown[] }>(data: TObj[], childKey: TKey) {
    // ...
}

Answer №3

Don't complicate things unnecessarily. Just utilize a Parent array

function getMaxNumChildren<T>(data: Parent[]T[], keyGetter: (obj: T) => Array<unknown>) {
  return data.reduce((max: number, parent: ParentT) => {
    return Math.max > (keyGetter(parent.children).length ?, max : parent.children.length;);
  }, 0);
}


Improved approach

It is more effective to use a callback function instead of manipulating the type system.

function getMaxNumChildren<T>(data: T[], keyGetter: (obj: T) => Array<unknown>) {
  return data.reduce((max: number, parent: T) => {
    return Math.max(keyGetter(parent).length, max);
  }, 0);
}

To apply this method:

console.log(getMaxNumChildren<Parent>(parents, (p) => p.children));

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

When a new array object is added to a nested array in a React Redux reducer, the array in the store is correctly updated but the React component

I am brand new to React and redux. Currently, I have a task where I need to implement workflows with tasks inside them. While I successfully managed to add a new workflow object to the state array, I encountered a problem when trying to add a new task - it ...

Different Angular 2 components are resolved by routes

Consider this scenario: Upon navigating to the URL /product/123, the goal is to display the ProductComponent. This is how it's currently configured: RouterModule.forRoot([ { path: 'product/:productId', ...

Checkbox: Customize the appearance of the root element

I am trying to modify the root styles of a Checkbox component, but it is not working as expected. Here is my code: <CheckboxItem onChange={()} checked={isChecked} label="Show Checkb ...

Encountering browser freezing issues with a Next.JS app when trying to add an input field

I am currently utilizing Next.JS to construct a form with two inputs. I have followed the traditional React approach for input text reading and validation. "use client" import { firebaseApp } from '@/app/firebase'; import React, { useCa ...

Error: The render view is unable to read the 'vote' property of a null object

Within this component, I am receiving a Promise object in the properties. I attempt to store it in state, but upon rendering the view, I encounter the error message "TypeError: Cannot read property 'vote' of null." Seeking a solution to my predic ...

All constructors at the base level must share a common return type

I am looking to convert my JSX code to TSX. I have a snippet that refactors a method from the react-bootstrap library: import {Panel} from 'react-bootstrap'; class CustomPanel extends Panel { constructor(props, context) { super(props ...

A guide on how to identify the return type of a callback function in TypeScript

Looking at this function I've created function computedLastOf<T>(cb: () => T[]) : Readonly<Ref<T | undefined>> { return computed(() => { const collection = cb(); return collection[collection.length - 1]; }); } Thi ...

Ways to avoid Next.js from creating a singleton class/object multiple times

I developed a unique analytics tool that looks like this: class Analytics { data: Record<string, IData>; constructor() { this.data = {}; } setPaths(identifier: string) { if (!this.data[identifier]) this.da ...

Stop automatic scrolling when the keyboard is visible in Angular

I have created a survey form where the user's name appears on top in mobile view. However, I am facing an issue with auto scroll when the keyboard pops up. I want to disable this feature to improve the user experience. <input (click)="onFocusI ...

What is the best way to determine the final letter of a column in a Google Sheet, starting from the first letter and using a set of

My current approach involves generating a single letter, but my code breaks if there is a large amount of data and it exceeds column Z. Here is the working code that will produce a, d: const countData = [1, 2, 3, 4].length; const initialLetter = 'A&a ...

Tips for ensuring all files are recognized as modules during the transition of an established NodeJS project to TypeScript

I'm diving into TypeScript as a newcomer and I am exploring the process of transitioning a large, existing NodeJS codebase that is compliant with ES2017 to TypeScript. Here's a glimpse at my tsconfig.json: { "compilerOptions": { "mo ...

Discover the location of items within an array

Currently, I am working with a JSON object that has the following structure. My objective is to determine the index based on ID in order to retrieve the associated value. The indexOf function appears to be suitable for arrays containing single values, but ...

Is there a way to display a variety of images on separate divs using *ngFor or another method?

I have created a custom gallery and now I would like to apply titration on the wrapper in order to display a different image on each div. Currently, my code is repeating a single image throughout the entire gallery. HTML <div class="wrapper" ...

Running terminal commands in typescript

Can Typescript be used to run commands similar to JavaScript using the Shelljs Library? ...

What is the best way to change the number 123456789 to look like 123***789 using either typescript or

Is there a way to convert this scenario? I am working on a project where the userID needs to be displayed in a specific format. The first 3 characters/numbers and last 3 characters/numbers should be visible, while the middle part should be replaced with ...

Angular HTTP requests are failing to function properly, although they are successful when made through Postman

I am attempting to send an HTTP GET request using the specified URL: private materialsAPI='https://localhost:5001/api/material'; setPrice(id: any, price: any): Observable<any> { const url = `${this.materialsURL}/${id}/price/${price}`; ...

"An issue has been noticed with Discord.js and Discordx VoiceStateUpdate where the return

Whenever I attempt to retrieve the user ID, channel, and other information, I receive a response of undefined instead of the actual data import { VoiceState } from "discord.js"; import { Discord, On } from "discordx"; @Discord() export ...

Tips for hiding a sidebar by clicking away from it in JavaScript

My angular application for small devices has a working sidebar toggling feature, but I want the sidebar to close or hide when clicking anywhere on the page (i.e body). .component.html <nav class="sidebar sidebar-offcanvas active" id="sid ...

Is it possible for Typescript to automatically determine the exact sub-type of a tagged union by looking at a specific tag value?

I have an instance of type Foo, which contains a property bar: Bar. The structure of the Bar is as follows: type ABar = {name: 'A', aData: string}; type BBar = {name: 'B', bData: string}; type Bar = ABar | BBar; type BarName = Bar[&apos ...

Getter and setter methods in Angular Typescript are returning undefined values

I am facing a challenge in my Angular project where I need a property within a class to return specific fields in an object. Although I have implemented this successfully in .Net before, I am encountering an issue with getting an "Undefined" value returned ...