How can Typescript generics verify the value type for a specific key in a generic object?

I am facing an issue with a function called sortData. This function takes in a key and is designed to sort an array of objects based on the values for that key:

function compare(v1: string | number, v2: string | number) {
  return (v1 < v2 ? -1 : v1 > v2 ? 1 : 0);
}    

function sortData<T>(data: Array<T>, column: keyof T, direction: string): Array<T> {
  if (direction === '' || column === '') {
    return data;
  } else {
    return [...data].sort((a, b) => {
      const res = compare(a[column], b[column]);
      return direction === 'asc' ? res : -res;
    });
  }
}

The challenge arises from the fact that the type of data[0][column], which represents a value in an object of type T, may not necessarily be a string | number that can be passed through the compare function. The type T could contain values other than just string | number, and it's unknown beforehand. Nevertheless, I would like the compiler to generate an error if I attempt to use sortData with a column that does not hold a string | number value.

How can I verify the type of a generic object's value in this scenario?

Answer №1

My preferred method in this scenario is to introduce an additional generic type parameter K that corresponds to the key of the T you are providing. Then, I apply a constraint on T to ensure it can be assigned to a type with a property of type string | number at the key K. This constraint involves using the Record utility type:

function sortData<T extends Record<K, string | number>, K extends keyof T>(
    data: Array<T>, column: K, direction: string): Array<T> {
    if (direction === '' || column === '') {
        return data;
    } else {
        return [...data].sort((a, b) => {
            const res = compare(a[column], b[column]);
            return direction === 'asc' ? res : -res;
        });
    }
}

The code now compiles without errors and maintains the desired functionality when called:

interface MyType {
    foo: number, //sortable
    bar: { x: number, y: number } //not sortable
}
var x: MyType[] = [
    { foo: 5, bar: { x: 1, y: 2 } },
    { foo: 3, bar: { x: 3, y: 2 } }
]
console.log(sortData(x, "foo", 'asc')) // valid
console.log(sortData(x, 'bar', 'asc')) // error!
// ----------------> ~
// Argument of type 'MyType[]' is not assignable to parameter 
// of type 'Record<"bar", string | number>[]'.

Despite the slightly strange error message focusing on the array rather than the column, we can improve clarity by modifying the constraint on column. By creating a utility type that utilizes a conditional type for filtering, we can address this issue. Here's how it can be implemented:

type StrNumKeys<T> = keyof {
    [K in keyof T as T[K] extends string | number ? K : never]: any
}

Using key remapping, inappropriate keys can be filtered out. While there are alternative ways to define StrNumKeys, let's proceed with testing it against your MyType type:

type Test = StrNumKeys<MyType>;
// type Test = "foo"

Everything appears to be correct. Therefore, it becomes unnecessary for column to be generic when employing this utility type:

function sortData<T extends Record<StrNumKeys<T>, string | number>>(
    data: Array<T>, column: StrNumKeys<T>, direction: string): Array<T> {
    if (direction === '' || column === '') {
        return data;
    } else {
        return [...data].sort((a, b) => {
            const res = compare(a[column], b[column]);
            return direction === 'asc' ? res : -res;
        });
    }
}

This version also successfully compiles since a[column] effectively accesses elements from

Record<StrNumKeys<T>, string | number>
with keys of type StrNumKeys<T>. Consequently, the compiler recognizes these values as compatible with string | number. As a result, when calling the function, better error messages are generated for invalid cases:

console.log(sortData(x, "foo", 'asc')); // valid
console.log(sortData(x, 'bar', 'asc')); // error
// -------------------> ~~~~~
// Argument of type '"bar"' is not assignable to parameter of type '"foo"'.

The choice between these two approaches is yours: the two-generic variant relies on more common TypeScript features, while the single-generic variant employs conditional types in a more intricate manner. If the latter option leads to unexpected behavior in specific scenarios, reverting to the former might be advisable.

Link to Playground for Code

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

Issue with useEffect() not receiving prop

Currently diving into the world of React and still in the learning process. I recently integrated a useEffect() hook in my code: import { fetchDetails } from "../../ApiServices/Details"; export interface RemoveModalProps { id: number; i ...

The absence of type-safety in the MUI System sx is a glaring issue

I want to ensure that the TypeScript compiler can identify typos in my MUI System sx props: import { Box, SxProps, Theme, Typography } from '@mui/material' const sxRoot: SxProps<Theme> = { width: '100vw', height: '10 ...

How to align an image in the center of a circular flex container

I'm facing an issue in my Angular project where I have the following code snippet: onChange(event: any) { var reader = new FileReader(); reader.onload = (event: any) => { this.url = event.target.result; }; reader.readAsData ...

Determine the character count of the text within an *ngFor loop

I am a beginner in Angular (8) and I am trying to determine the length of the input value that I have created using a *ngFor loop as shown below: <div *ngFor="let panel of panels; index as i" class="panel" [id]="'panel-' + panel.id"> & ...

In TypeScript, if all the keys in an interface are optional, then not reporting an error when an unexpected field is provided

Why doesn't TypeScript report an error when an unexpected field is provided in an interface where all keys are optional? Here is the code snippet: // This is an interface which all the key is optional interface AxiosRequestConfig { url?: string; m ...

What are the reasons behind the unforeseen outcomes when transferring cookie logic into a function?

While working on my express route for login, I decided to use jwt for authentication and moved the logic into a separate domain by placing it in a function and adjusting my code. However, I encountered an issue where the client side code was unable to read ...

Troubleshooting the Issue with Angular Material Dialog Imports

Hey there, I'm trying to utilize the Angular Material dialog, but I'm encountering issues with the imports and I can't seem to figure out what's wrong. I have an Angular Material module where I imported MatDialog, and I made sure to i ...

Error encountered: Uncaught SyntaxError - An unexpected token '<' was found while matching all routes within the Next.js middleware

I am implementing a code in the middleware.ts file to redirect users to specific pages based on their role. Here is the code snippet: import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' import { get ...

Include type declarations for property values that resemble arrays in a generic object

Imagine you have a function that: receives an object with multiple properties and a property name; checks if the property holds an array; if it does, performs an action (such as printing the values it contains) Here's an illustration: function pro ...

What is the best way to extract a specific value from a JSON object?

I'm currently working on building a marketplace using Angular. The main marketplace page is already set up and populated with data from a remote JSON file created with mockapi. However, I've encountered an issue when trying to display a single ra ...

Replace a portion of text with a RxJS countdown timer

I am currently working on integrating a countdown timer using rxjs in my angular 12 project. Here is what I have in my typescript file: let timeLeft$ = interval(1000).pipe( map(x => this.calcTimeDiff(orderCutOffTime)), shareReplay(1) ); The calcTim ...

Transforming encoded information into a text format and then reversing the process

I am facing an issue with storing encrypted data in a string format. I have tried using the TextEncoder method but it seems to be creating strings with different bytes compared to the original ArrayBuffer. Here is the line causing the problem: const str ...

What are the steps to utilize a personalized validation access form within a component?

I created a unique validator to verify if an email already exists in the database before saving a new user, however, it appears that the validator is not functioning properly. Here is my template: <form class="forms-sample" #f="ngForm" (ngSubmit)="onS ...

Executing several asynchronous functions in Angular 2

I am currently developing a mobile app and focusing on authentication. In order to display data to the user on my home page, I need to connect to various endpoints on an API that I have created. Although all endpoints return the correct data when tested i ...

Adjust the scroll bar to move in the reverse direction

I'm trying to modify the behavior of an overlay div that moves when scrolling. Currently, it works as expected, but I want the scroll bar to move in the opposite direction. For example, when I scroll the content to the right, I want the scroll bar to ...

Customizing the HTMLElement class to modify particular attributes

Is there a way to modify the behavior of an HTMLElement's scrollTop property by adding some extra logic before updating the actual value? The common approach seems to be deleting the original property and using Object.defineProperty(): delete element. ...

Stop the transmission of ts files

Recently, I noticed that when using node JS with Angular-CLI, my .ts files are being transmitted to the client through HTTP. For example, accessing http://localhost/main.ts would deliver my main.ts file to the user. My understanding is that ts files are s ...

Steps for creating a click event for text within an Ag-Grid cell

Is there a way to open a component when the user clicks on the text of a specific cell, like the Name column in this case? I've tried various Ag-Grid methods but couldn't find any that allow for a cell text click event. I know there is a method f ...

Is there a way to verify whether a key within an Interface corresponds to a class that is a subclass of a specific Parent

Is there a method in typescript to ensure that a property in an interface must be of type "child subclass C, which extends class P"? example.ts import { P } from '/path/to/types' class C extends P { ... } types.ts // `C` cannot be accessed ...

The parameter of type 'void' cannot be assigned to the parameter of type 'PathParams'

Established the route handler and encountered an issue while integrating it into my route. import {Application, NextFunction} from 'express'; import {container} from 'tsyringe'; const routeConstantsArray = { }; const constants: any ...