How should one properly format an array of objects with elements that are different types of variations?

I am currently developing a versatile sorting module using TypeScript. This module will take in an array of data objects, along with a list of keys to sort by. Once the sorting process is complete, the array will be sorted based on the specified keys.

To start off, I have defined two types:

export type Comparer<T>: (a: T, b: T) => number;

export type SortKey<T> = {
    key: keyof T,
    order: 'ascending' | 'descending',
    comparer: Comparer<T[keyof T]>
};

During testing, I encountered a problem with the above setup. Let's consider an example:

// Assuming numberComparer has the type (a: number, b: number) => number
import { numberComparer } from 'somewhere';

type Person = {
    id: number;
    name: string;
};

const sortKey: SortKey<Person> = {
    key: 'id',
    order: 'ascending',
    comparer: numberComparer
};

This resulted in a TypeScript error stating that the comparer property should be of type Comparer<string | number>, but numberComparer does not match this requirement. The issue arises because the type inference is unable to determine that since the value of key is 'id', it should compare numbers due to id being a numeric field.

To address this, I introduced version 2 of the type:

export type SortKey<T, K extends keyof T = keyof T> = {
    key: K,
    order: 'ascending' | 'descending',
    comparer: Comparer<T[K]>
};

In this updated version, the example would function correctly if we explicitly specify K. While I believe TypeScript should be able to infer this implicitly, for now we can rely on explicit declarations:

const sortKey: SortKey<Person, 'id'> = {
    key: 'id',
    order: 'ascending',
    comparer: numberComparer
};

The error has now been resolved. However, a new challenge emerges when examining the generic sort function:

export function sort<T>(data: T[], keys: SortKey<T>[]);

While implementing the function body does not result in errors, consumers of the function are restricted in usage. For instance, attempting the following leads to issues:

const myKeys: SortKey<Person>[] = [
    {
        key: 'id',
        order: 'descending',
        comparer: numberComparer
    },
    {
        key: 'name',
        order: 'ascending',
        comparer: stringComparer
    }
];

This results in the original TypeScript errors, where the provided comparer is specific to a certain type (number or string), yet the comparer property accepts all potential keys within the Person type.

A possible solution could involve creating a "master" comparer with the necessary type constraints, which would then handle parameter type-checking and delegate calls to specific comparers accordingly. Although effective, this approach may seem inelegant.

Therefore, I turn to my fellow TypeScript enthusiasts for any alternative solutions to this dilemma. Your insights are truly appreciated as always.

Answer №1

It appears that you are looking to make SortKey<T> a combination of union types where each member is based on SortKey<T, K> for every K in keyof T. Essentially, you want SortKey<T, K> to be applied across unions within K. This results in the following behavior:

type SortKeyPerson = SortKey<Person>;
/* type SortKeyPerson = {
    key: "id";
    order: 'ascending' | 'descending';
    comparer: Comparer<number>;
} | {
    key: "name";
    order: 'ascending' | 'descending';
    comparer: Comparer<string>;
} */

This indicates the need for a mechanism that can distribute over unions. There are several approaches to achieving this, with one common method involving the use of a distributive object type as detailed in microsoft/TypeScript#47109. This entails creating a mapped type that iterates over the properties of T, followed by indexing into it using the complete set of keys. In essence, if you have a type F<K> which needs to be distributed over unions in K, you can express this as {[P in K]: F<P>}[K].

In the case at hand, the implementation looks like this:

type SortKey<T> = { [K in keyof T]: {
    key: K,
    order: 'ascending' | 'descending',
    comparer: Comparer<T[K]>
} }[keyof T]

You can verify that this produces the desired union type mentioned earlier. Consequently, your examples can now function as intended:

const myKeys: SortKey<Person>[] = [
    {
        key: 'id',
        order: 'descending',
        comparer: numberComparer
    },
    {
        key: 'name',
        order: 'ascending',
        comparer: stringComparer
    }
];

Playground link to 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

Unable to locate module within Typescript

Hello everyone, I am facing a problem similar to this one: I have an app written in TypeScript, and within it, I have imported import { Component } from '@angular/core'; import {CORE_DIRECTIVES} from '@angular/common'; import { MODA ...

Creating a HTML element that functions as a text input using a div

I am encountering an issue with using a div as text input where the cursor flashes at the beginning of the string on a second attempt to edit the field. However, during the initial attempt, it does allow me to type from left to right. Another problem I am ...

What is the best way to retrieve the Base64 string from a FileReader in NextJs using typescript?

My goal is to extract a string from an object I am receiving in the response. const convertFileToBase64 = (file: File): Promise<string> => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.r ...

Angular has its own unique way of handling regular expressions through its TypeScript

Considering the creation of an enum to store various regex patterns in my application for enhanced code reuse. For example: export enum Regex { ONE_PATTERN = /^[example]+$/g, ANOTHER_PATTERN = /^[test]{5,7}$/g } However: Encountering the TS90 ...

What is the best way to specify types for a collection of objects that all inherit from a common class?

I am new to typescript and may be asking a beginner question. In my scenario, I have an array containing objects that all extend the same class. Here is an example: class Body{ // implementation } class Rectangle extends Body{ // implementation } class ...

Guide on extracting just the key and its value from a Filter expression in a DynamoDB Query using Typescript

Presented here is a filter expression and Key Condition. The specific set of conditions are as follows: {"Age":{"eq":3},"Sex":{"eq":"MALE"}} const params: QueryCommandInput = { TableName: my_tab ...

When attempting to access http://localhost:3000/traceur in Angular 2 with the angular-in-memory-web-api, a 404 (Not Found) error

Hello, I'm encountering an issue with angular-in-memory-web-api. I have attempted to use angular2-in-memory-web-api in SystemJS and other solutions, but without success. I am currently using the official quickstart template. Thank you for any assistan ...

Tips for preventing duplicate imports in Sass with the @use rule in Webpack

My sass modules have the ability to import each other as shown in the examples below: // LinearLayout.scss @mixin LinearLayout { ... } linear-layout { @include LinearLayout; } // ScrollView.scss @use "LinearLayout" as *; @mixin ScrollView { ...

Vue 3 Electron subcomponents and routes not displayed

Working on a project in Vue3 (v3.2.25) and Electron (v16.0.7), I have integrated vue-router4 to handle navigation within the application. During development, everything works perfectly as expected. However, when the application is built for production, the ...

Is it possible to modify the CSS injected by an Angular Directive?

Is there a way to override the CSS generated by an Angular directive? Take, for instance, when we apply the sort directive to the material data table. This can result in issues like altering the layout of the column header. Attempting to override the CSS ...

Implementing an Asynchronous Limited Queue in JavaScript/TypeScript with async/await

Trying to grasp the concept of async/await, I am faced with the following code snippet: class AsyncQueue<T> { queue = Array<T>() maxSize = 1 async enqueue(x: T) { if (this.queue.length > this.maxSize) { // B ...

Dealing with a multi-part Response body in Angular

When working with Angular, I encountered an issue where the application was not handling multipart response bodies correctly. It seems that the HttpClient in Angular is unable to parse multipart response bodies accurately, as discussed in this GitHub issue ...

Tips for handling catch errors in fetch POST requests in React Native

I am facing an issue with handling errors when making a POST request in React Native. I understand that there is a catch block for network connection errors, but how can I handle errors received from the response when the username or password is incorrec ...

Error message: Cypress Vue component test fails due to the inability to import the Ref type (export named 'Ref' is missing)

Recently, I created a Cypress component test for my Vue component by mounting it and verifying its existence. The component utilizes Vue's Ref type and is structured as a TypeScript component. However, during the test execution, Cypress encountered a ...

Problem with Typescript: The type '{ x;y }' is required to have a '[Symbol.iterator]()' method

Just starting out with Typescript and tackling the task of converting a React project from JavaScript to TypeScript. I've been diving into various posts for guidance, but I feel like I'm going in circles. Any assistance would be greatly appreci ...

Using the appropriate asynchronous action in Redux with the Redux-Thunk middleware

While learning middleware redux-thunk, I created a simple React App. However, I am facing a transpilation issue due to an incorrect type of async action fetchPosts in the interface PostListProps. Below is the code snippet of the functional component where ...

Is Angular's ngOnChanges failing to detect any changes?

Within one component, I have a dropdown list. Whenever the value of the dropdown changes, I am attempting to detect this change in value in another component. However, I am encountering an unusual issue. Sometimes, changing the dropdown value triggers the ...

Is there a way to make a peer dependency optional in a project?

In the process of developing module A, I have implemented a feature where users can choose to inject a Winston logger into my module. As a result, winston becomes a peer dependency. However, when attempting to include module A in another module without th ...

Troubleshoot: Issue with injecting external component into another component using directive in Angular 2

I need the child component template to be loaded into the parent component template. (calling them child and parent for simplicity) Here is the child component: import {Component,Directive, ElementRef, Input} from '@angular/core'; import {IONIC ...

Error in custom TypeScript: Incorrect error instance detected within the component

I encountered a unique issue with my custom Error export class CustomError extends Error{ constructor(message: string) { super(message); Object.setPrototypeOf(this, CustomError.prototype); this.name = "CustomError"; } Furthermore ...