Implement dynamic typing in the sort function to restrict it to only accept number properties

I need help creating a pipe that can sort an array of objects based on a specified property. While I have managed to make it work, I am encountering a type error in my code. Here is the snippet:

export const sortByProperty = <T>(a: T, b: T, property: keyof T): number => {
  if (isNaN(a[property])) return isNaN(b[property]) ? 0 : -1; // error:  Type 'T[string]' is not assignable to type 'number'
  if (isNaN(b[property])) return 1;
  return a[property] - b[property]; // error: The right-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.
};

@Pipe({
  name: 'sortBy',
  standalone: true
})
export class SortByPipe implements PipeTransform {
  transform<T>(items: T[], property: keyof T): T[] {
    return [...items].sort((a, b) => sortByProperty(a, b, property));
  }
}

Here is an example of how to use it:

<div *ngFor="let product of productList | sortBy: 'value'">...</div>

The data in productList looks like this:

productList = [
  { order: 0, value: 10, title: 'some product' },
  { order: 1, value: 1000, title: 'some other product' },
  { order: 2, value: 50, title: 'a great product' },
];

The issue here is that the specified property can be any of the properties of items in the productList. However, I want to restrict it to only properties that will contain numerical values to avoid errors. You can see the problem in action in this Stackblitz example. (remove the a in front of @ts-ignore to see it working)

Answer №1

To tackle this issue, my preferred approach would involve making the sortByProperty function generic in the type K associated with the property parameter, instead of using the type T</code for the <code>a and b parameters. It would look something like this:

const sortByProperty = <K extends PropertyKey>(
    a: Record<K, number>,
    b: Record<K, number>,
    property: K
): number => {
    if (isNaN(a[property])) return isNaN(b[property]) ? 0 : -1;
    if (isNaN(b[property])) return 1;
    return a[property] - b[property];
};

It's important to note that K is being constrained to a keylike type. Additionally, a and b are of type Record<K, number> using the Record<K, V> utility type. This ensures that the properties passed in for a and b must have a number-valued property at the key specified by property.


There is a way in TypeScript to define "only the keys of T whose values are numbers," which I usually refer to as KeysMatching<T, V>. One possible implementation is shown below:

type KeysMatching<T, V> = keyof {
    [K in keyof T as T[K] extends V ? K : never]: any
}

However, the challenge is that the compiler may not fully comprehend what this operation does for arbitrary generic types like T. As a result, you could encounter similar errors within a function implementation using this approach:

const sortByProperty = <T extends object>(
    a: T,
    b: T,
    property: KeysMatching<T, number>
): number => {
    if (isNaN(a[property])) return isNaN(b[property]) ? 0 : -1; // error
    if (isNaN(b[property])) return 1; // error
    return a[property] - b[property]; // error
};

An open feature request at microsoft/TypeScript#48992 aims to address this limitation by natively representing KeysMatching for better compiler understanding. Until then, unless there is a specific reason to proceed otherwise, I recommend opting for the generic approach using K instead of T.

Explore the code in the TypeScript Playground

Answer №2

To elaborate on the solution provided by @jcalz, there are various ways to make the KeysMatching<T, V> type function properly, enabling accurate intellisense suggestions for number type properties.

One alternative approach to defining the KeysMatching type, albeit more cumbersome (but effective), is as follows:

type KeysMatchingType<T extends object, V> = {
  // select keys of T's properties with types extending V
  [K in keyof T]-?: T[K] extends V ? K : never;
}[keyof T];

This type can then be utilized in the following manner:

const sortByProperty = <T extends object, K extends KeysMatchingType<T, number>>(
  a: T,
  b: T,
  property: K
): number => {
  if (isNaN(a[property])) return isNaN(b[property]) ? 0 : -1;
  if (isNaN(b[property])) return 1;
  return a[property] - b[property];
};

const a = { x: "1", y: 2 }
const b = { x: "2", y: 4 }

// intellisense will now only suggest "y" as property name when using a and b
sortByProperty(a, b, "y")

For further exploration and testing, you can access the Playground link

This implementation is functional from Typescript 5.1.6 onwards (not tested on earlier 5.1.X versions). Prior versions, including 5.0.4, encounter the same issue as mentioned in @jclaz's explanation. If you are unable to utilize 5.1.6 yet, it would be advisable to follow @jcalz's Record<K, number> approach.

It is worth noting that despite efforts to identify the specific changes in the typescript 5.1 release notes that facilitated this solution, no clear indication was found. Any insights on this matter would be welcomed for updating purposes.

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

Create a TypeScript declaration file for a JavaScript dependency that contains an exported function

I am currently utilizing a dependency called is-class in my TypeScript project. Unfortunately, this particular dependency does not come with a @types definition. As a workaround, I have been using a custom .d.ts file with declare module 'is-class&apos ...

Is there a way to enhance a pre-existing component in Angular?

Trying to integrate an Angular component and wanting to implement a unique template. By referencing the documentation, it is possible to customize the menuComponent by utilizing the built-in TextInputAutocompleteMenuComponent component to apply a custom t ...

Guide to Utilizing the Dracula Graph Library in Angular

Recently, I stumbled upon a JavaScript library that seems to be an ideal fit for my project. The library can be found at: After installing the necessary libraries using npm - npm i raphael graphdracula - new folders were created in node_modules and th ...

Troubleshooting problem with GZIP in Angular 2 application deployment

I have developed an Angular 2 TypeScript application. I am using Firebase for hosting and Cloudflare for optimizing speed, caching, and security. When checking the browser header, it indicates: accept-encoding:gzip, deflate, sdch, br The app.js file has ...

Addressing the spacing and alignment issues within the progress bar

I need some help with my Angular app. I am currently working on creating a progress bar, but I am facing some issues with the alignment of the bars. Here is the code snippet that I have used: CSS: .progressbar { position: relative; height: 56px; mar ...

Receiving an error in Typescript when passing an object dynamically to a React component

Encountering a typescript error while attempting to pass dynamic values to a React component: Error message: Property 'title' does not exist on type 'string'.ts(2339) import { useTranslation } from "react-i18next"; import ...

What is the best approach to integrate react-hooks, redux, and typescript seamlessly?

Struggling to seamlessly integrate React-hooks, Redux, and Typescript. It's a never-ending cycle of fixing one error only for another to pop up. Can anyone pinpoint what the root issue might be? Currently facing the following error related to my red ...

Angular 2 fails to identify any modifications

Within my template, the links are set to change based on the value of the 'userId' variable. <nav> <div class="nav-wrapper"> <a href="#" class="brand-logo"><img src="../../public/images/logo.png" alt="" /></a> ...

Divide Angular module

In my Angular application, I have set up pages with a consistent header and sidebar structure. However, the main content on these pages may change based on different routes. To keep things organized, I decided to create a central page outlet where the main ...

Leveraging Angular's HTTPClients: merging multiple HTTP requests with Observables to optimize asynchronous operations

In my HTML code, I have the following: <ng-container *ngIf="lstSearchResults|async as resultList; else searching"> ... <cdk-virtual-scroll-viewport class="scroll-container"> <div *cdkVirtualFor="let search of resultList" class="card ...

A guide to implementing previousState in React hooks with TypeScript

I am trying to grasp the concept of using previous state with react hooks in typescript. The code snippet provided below does function, however, it throws an error stating: Type 'number' is not assignable to type 'HTMLDivElement'. Whi ...

Tips on troubleshooting the issue when attempting to use a hook in your code

I am trying to implement a hook to manage the states and event functions of my menus. However, when I try to import the click function in this component, I encounter the following error: "No overload matches this call. The first of two overloads, '(p ...

Angular - Error: Unable to assign value to a reference or variable

I am currently working on an app built with Angular 8. app.component.html <div> <label class="form-check-label f-14"> <input type="checkbox" class="form-check-input" name="isAgree" [(ngModel)]="isAgree" #isAgree="ngModel"/> ...

Using TypeScript to eliminate duplicate values when constructing an array with various properties

Recently, I received an array from an API that has the following structure: results = [ {name: 'Ana', country: 'US', language: 'EN'}, {name: 'Paul', country: 'UK', language: 'EN'}, {name: & ...

What is the best way to include a button at the bottom of a Material UI table?

I've been working with Material UI in React TypeScript and I'm having trouble adding a button at the bottom that leads to a form. Despite my attempts, I haven't been successful. Can someone please help me with this? I just need a simple butt ...

Utilizing various filters and sorting options on API response within Angular 8

Upon receiving the following API response: [ { "imgPaths":[ "gallery/products/55ccb60cddb4d9bded02accb26827ce4" ], "_id":"5f3e961d65c6d591ba04f3d3", "productName":" ...

When trying to access the "form" property of a form ElementRef, TypeScript throws an error

I've encountered an issue with accessing the validity of a form in my template: <form #heroForm="ngForm" (ngSubmit)="onSubmit()"> After adding it as a ViewChild in the controller: @ViewChild('heroForm') heroForm: ElementRef; Trying ...

Pagination in Angular 2

My current implementation involves using ngx pagination. I have successfully changed the navigation list by passing (pageChange)="p = $event". However, I now want to send the p value to a function like (pageChange)="pageChange(p)" and then set p = $event w ...

Avoid triggering the onClick event on specific elements in React by utilizing event delegation or conditional rendering

programming environment react.js typescript next.js How can I prevent the onClick process from being triggered when the span tag is pressed? What is the best approach? return ( <div className="padding-16 flex gap-5 flex-container" ...

angular proxy configuration malfunctioning

I'm struggling to figure out where my mistake is. Despite trying the solution provided in an answer, the issue persists. Here are a few related threads I've explored: Angular-CLI proxy to backend doesn't work Configure Angular-cli proxy f ...