Limit object key type in generics using Typescript

I'm currently facing a challenge while trying to develop a function that arranges an array of objects based on a specific object key. TypeScript keeps throwing an error indicating that the object type is not "any", "number", "bigint" or an "enum". I attempted to restrict the type of the key to only accept number. Surprisingly, it partially functions when invoking the function, as in cases where the argument contains a property that is not a number, an error message pops up.

Argument of type '"b"' is not compatible with parameter of type 'KeysOfType<{ a: number; b: string; c: number; }, number>'.ts(2345)

Nevertheless, I fail to comprehend why TypeScript fails to recognize that the property is indeed a number within the function itself, leading to errors. How can I overcome/resolve this predicament? Could my approach towards typing be incorrect?

Displayed below are the code snippets containing the aforementioned errors:

type KeysOfType<T, KT> = {
  [K in keyof T]: T[K] extends KT ? K : never;
}[keyof T];

export function sortArrayByKey<T, K extends KeysOfType<T, number>>(
  list: T[],
  propertyKey: K
) {
  // The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.ts(2362)
  // The right-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.ts(2363)
  return list.slice().sort((a, b) => (a[propertyKey] - b[propertyKey]));
}

// works
sortArrayByKey([{a: 1, b: 'asd', c: 3.0}, {a: 2, b: '123', c: 2.1}], 'a');

// Argument of type '"b"' is not compatible with parameter of type 'KeysOfType<{ a: number; b: string; c: number; }, number>'.ts(2345)
sortArrayByKey([{a: 1, b: 'asd', c: 3.0}, {a: 2, b: '123', c: 2.1}], 'b');

Answer №1

Here you can find a similar inquiry, but this question delves into the use of a function constraint rather than a number.

While TypeScript can verify the passed propertyKey during the function call stage, it lacks the ability to validate whether b[propertyKey] is a number within the function body. The type a[propertyKey] is inferred as T[K], which does not indicate whether it is a number. This creates ambiguity since there is no constraint for T. TypeScript only recognizes that K is an allowable type for indexing T.

To address this issue, a constraint should be applied to T. One solution is to define:

T extends Record<PropertyKey, number>
. It may seem restrictive, considering that not all values in an object assigned to
T</code are numbers. Therefore, a transitional function is needed to transform any object <code>T
to
T extends Record<string, number>
.

The transitional function:

const toNumber = <
  Obj extends Record<string, any>,
  Key extends KeysOfType<Obj, number>
>(obj: Obj, key: Key): number => obj[key]

It's noteworthy that an explicit return type number is utilized here. Without it, the return type of this function defaults to T[K]. However, by specifying number, TypeScript permits us to include checks on assignability.

Now, we can formulate our callback for Array.prototype.sort and the main function:

const callback =
  <Obj,>(propertyKey: KeysOfType<Obj, number>) =>
    (a: Obj, b: Obj) =>
      toNumber(a, propertyKey) - toNumber(b, propertyKey);

const sortArrayByKey = <T, K extends KeysOfType<T, number>>(
  list: T[],
  propertyKey: K
) => [...list].sort(callback(propertyKey))

Interactive demo

Answer №2

While I believe I have a solution for this, there may be alternative methods that are more efficient:

type KeysOfType<T, KT> = {
  [K in keyof T]: T[K] extends KT ? K : never;
}[keyof T];

export function sortArrayByKey<T, K extends KeysOfType<T, number>>(
  list: T[],
  propertyKey: K
) {
  return list
    .slice()
    .sort(
      (a, b) =>
        (a[propertyKey] as unknown as number) -
        (b[propertyKey] as unknown as number)
    );
}

In this code snippet, I am essentially informing TypeScript about the type of a[propertyKey], emphasizing that it is indeed a number despite any confusion on its part.

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

AmCharts 4: defining the grid interval

I am currently working on creating a polar chart to showcase Satellite data. https://i.sstatic.net/DNUZ1.jpg However, I am facing a challenge with setting the grid size to be displayed in increments of 45 degrees. Despite trying various amcharts 4 functi ...

The value of the local variable remained unchanged despite the occurrence of global events (window.onresize)

The value of the local variable remained unchanged despite global events (window.onresize) occurring. export class TestComponent implements OnInit { a: number = 0; b: number = 0; ngOnInit() { window.onresize = () => { ...

Is there a way to navigate to a specific component selector within an ngFor loop?

I have a scenario where I have multiple components running inside *ngFor on the same page. My goal is to create button links at the top of the page that, when clicked, will scroll to the corresponding component on the page. Below are the code snippets tha ...

The ng-change event fails to trigger when the date change event is activated

Hey there, I'm new to working with AngularJS. I have a scenario where I want a method to be triggered when I change the date, opening a modal. However, when I use the ng-change event, the method is not called upon date change. If I switch to using ng- ...

Prevent the use of a single equals sign in an if statement

Is it possible to prevent the use of a single equals sign in an if statement with TypeScript? I encountered an error in an Android codebase that should have occurred on iOS. The problem was caused by a shortcut in a styles file using (Platform.OS = 'i ...

Creating personalized breakpoints in Material UI using TypeScript

When using the createMuiTheme() function, you have the ability to update breakpoint values like this. const theme = createMuiTheme({ breakpoints: { values: { xs: 0, sm: 600, md: 960, lg: 1280, xl: 1920, }, }, }) ...

Struggling with implementing map() inside another map() within the render() method in React TypeScript

I am looking to display messages and their corresponding replies in a React application using TypeScript. The messages are stored in one array within the state, while the replies are stored in a separate array. This is my current code which is not renderi ...

Implementing unique union type in React: Effective ways to declare typescript type for prop value

I am currently facing an issue where I need to set a specific type for a prop value. However, the challenge lies in the fact that the types are string unions which can vary depending on the usage of the React Component. Let me provide you with the TypeScr ...

Ways to conceal table rows depending on their content

I am currently developing a project using Angular 2. One of the features includes a summary section that consolidates all the data from other sections. The summary is presented in a table format, with each row displaying the field name in the first colum ...

Exploring the process of dynamically incorporating headers into requests within react-admin

Currently utilizing react-admin with a data provider of simpleRestProvider. I am in need of a solution to dynamically add headers to requests based on user interactions. Is there a way to achieve this? Appreciate any assistance. Thank you! ...

Is it necessary to include a module in another module if it is not utilized in the template?

Is it necessary to import Module2 into Module1 if a component from Module2 is being used in Module1, but only in the typescript and not the template? For instance, as a @ContentChild(Component2) component2 like shown below (Note: both modules are secondary ...

The Angular 13 application encounters a "moment is not a function" error after importing an Angular 13 package

Upgrading a private library named privLib to Angular 13 has been my recent task in order to facilitate the migration of all other projects. However, an issue arises when this library is imported into another project where one of the services utilizes momen ...

Exploring the power of Angular 10 components

Angular version 10 has left me feeling bewildered. Let's explore a few scenarios: Scenario 1: If I create AComponent.ts/html/css without an A.module.ts, should I declare and export it in app.module.ts? Can another module 'B' use the 'A ...

Harness the power of moment.js in webpack 4 by integrating it with ts-loader

I'm facing an issue where moment.js isn't loading inside a TypeScript file during the webpack4 build process, no matter what I attempt. [tsl] ERROR in /app/Repository/JFFH.Site/Resources/Private/JavaScript/Angular/schedule/schedule.component.ts( ...

What is the best method for embedding my token within my user entity?

Currently, I am working on implementing a "forgot password" feature in my application. The idea is that when a user requests to reset their password, they will receive a token via email that expires after two hours. To prevent the generation of multiple to ...

What causes interface to generate TS2345 error, while type does not?

In the code below: type FooType = { foo: string } function fooType(a: FooType & Partial<Record<string, string>>) { } function barType(a: FooType) { fooType(a) } interface FooInterface { foo: string } function fooInterface(a: FooInt ...

React Typescript Context state isn't refreshing properly

Struggling to modify my context state, I feel like I'm overlooking something as I've worked with context in the past. The challenge lies in changing the 'isOpen' property within the context. You can view my code here: CodeSand **app.ts ...

Variations in assets configuration for ng serve and ng build in Angular 5

Is it possible to specify different asset arrays when using ng build? "assets": [ "assets", "favicon.ico", { "glob": "**/*", "input": "../externalDir", "output": "./app/", "allowOutsideOutDir": true } ] In my scenario, I only want ...

The function yields a resolved promise rather than returning data

I'm trying to use this function: const fetchAndMapData = (): Promise<Data> => { const data = fetch('https://jsonplaceholder.typicode.com/posts') .then((response) => response.json()) .then((items) => items.map((item: ...

Implement a click event listener in React.js

How can I implement a callback function for button clicks in my TypeScript code? This is the code snippet: export enum PAYMENT_METHOD { online, offline, } interface Props { paymentMethod: PAYMENT_METHOD; togglePaymentMethod: (paymentMethod: PAYM ...