Transform object properties into key-value objects using Typescript generics

When I receive a sorting object with a columnName and direction, I want to convert it into a key-value object for mongoose sorting.

The return values are not matching up and I can't seem to figure out what I'm missing.

These are the interfaces I am working with:

export enum SortDirection {
  asc = 'asc',
  desc = 'desc',
}

export class Sort<T> {
  columnName: keyof T
  direction: SortDirection
}

interface CriteriaRequestDto<T> {
  sort: Sort<T>
}

type SortQuery<T> = {
  [key in keyof T]?: SortDirection
}

buildSortQuery<T>(
    criteria: CriteriaRequestDto<T>,
  ): SortQuery<T> {
    if (!criteria || !criteria.sort) {
      return {}
    }
    const { columnName, direction } = criteria.sort

    return { [columnName]: direction }
  }

Here is my attempt on TS Playground

View Solution

Answer №1

Issue lies within this code snippet:

return { [columnName]: direction }
.

This syntax indicates {[prop: string]: SortDirection} when the expected result is

Record<keyof T, SortDirection>

Based on my observations, TypeScript struggles with computed object properties. Most of the time, it defaults to an indexed type like {[prop: string]: unknown}.

To address this issue, I introduced a new function called record which will output Record<keyof T, unknown>.

How did I solve this? By adding an additional overload to the function.

Is this solution foolproof? Not entirely, as best practice dictates defining multiple overloads for robustness.

Personally, I find overloading slightly safer than type casting using the as operator, but only marginally so.

Hence, explicitly specifying the return type of the record function was necessary.

function record<K extends Keys, V = unknown>(key: K, value: V): { [prop in K]: V }
function record<K extends Keys, V = unknown>(key: K, value: V) {
  return { [key]: value }
}

The following alternative notation does not function correctly:

function record<K extends Keys, V = unknown>(key: K, value: V): { [prop in K]: V } {
  return { [key]: value } // error
}

Here's a complete example:

interface Cat {
  age: number;
  breed: string;
}

enum SortDirection {
  asc = 'asc',
  desc = 'desc',
}

interface Sort<T> {
  columnName: keyof T
  direction: SortDirection
}

interface CriteriaRequestDto<T> {
  sort: Sort<T>
}

type Keys = string | number | symbol;

function record<K extends Keys, V = unknown>(key: K, value: V): { [prop in K]: V }
function record<K extends Keys, V = unknown>(key: K, value: V) {
  return { [key]: value }
}


type SortQuery<T> = Partial<Record<keyof T, SortDirection>>

function buildSortQuery<T>(
  criteria: CriteriaRequestDto<T>,
): SortQuery<T> {
  if (!criteria || !criteria.sort) {
    return {}
  }
  const { columnName, direction } = criteria.sort

  return record(columnName, direction)
}

const sortQuery: SortQuery<Cat> = {}
sortQuery.age = SortDirection.asc // OK 

const sort: Sort<Cat> = {
  columnName: "age", // OK
  direction: SortDirection.asc, // OK
}
const criteria: CriteriaRequestDto<Cat> = {
  sort: sort //ok
}
const query = buildSortQuery<Cat>(criteria)
query.age

Playground

UPDATE Please take a look here. Avoid using the as operator.

Answer №2

When the { [columnName]: direction } statement is used in Typescript, it interprets the type as { [x: string]: SortDirection; }. However, this type has a string index signature and cannot be assigned to SortQuery<T>, which only allows keys of T as properties. Essentially, Typescript assumes that if an object is created using a dynamic string key, then any string key should be allowed for that object. This assumption is incorrect.

Spreading Method

On the contrary, when a dynamic property is added to an existing object through spreading, Typescript retains the type of the original object and disregards the additional property.

This method may seem like a workaround but no errors will occur if you add your sort to an empty object.

return {
  ...{},
  [columnName]: direction
}

The returned object is interpreted by Typescript as type {}, which can be easily assigned to SortQuery<T> since all properties within SortQuery<T> are optional.

Using an Intermediate Variable

An alternative approach is to create an empty object with the SortQuery<T> type, assign the sort to it, and then return it.

const query: SortQuery<T> = {};
query[columnName] = direction;
return query;

You could also spread a property onto it. This method is similar to the "Spreading" solution but seems less like a hack since we explicitly define the initial object as being of type SortQuery<T> instead of just type {}.

const query: SortQuery<T> = {};

return {
  ...query,
  [columnName]: direction
}

Use of Assertion

Alternatively, you can assert that the return type is correct using as because you are confident that it is the right type even if Typescript does not recognize it.

return {
  [columnName]: direction
} as SortQuery<T>

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

What is the best way to utilize the .id property of a current User when using mongoose Model populate?

I have stored user information { "_id" : ObjectId("5f970abfe1c19f452cf715eb"), "username" : "Jean1", "email" : "<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="dab7aeb3bdbbb ...

Uncertainty surrounding the process of configuring the .env variable in a node.js/express environment

Recently, I encountered an issue while trying to establish a connection to my MongoDB database. Despite following the instructions in my learning modules, I was unable to make it work using the provided code snippet: const mongoose = require('mongoos ...

Exploring Mongoose: Implementing field selection and populating multiple levels simultaneously

I have successfully implemented population across multiple levels and field selection separately. However, I am having trouble combining them both simultaneously. Population across multiple levels Profile.findOne({ user: req.user.id }) .populate({ ...

importance of transferring information in URL encoded form

Recently, I started learning about node.js and API development. During a presentation, I was asked a question that caught me off guard. I had created a REST API (similar to a contact catalog) where data was being sent through Postman using URL encoded PO ...

The delay function in RxJS allows for waiting to return a value until a specific condition is met within a stream and

Currently, I am facing an issue with a method in my application that triggers a server request. This method has access to a stream from the redux-store and needs to execute a callback only when the result of the request is found in the mentioned stream. Th ...

Using React with an Array of Promises in Typescript

I have a function that looks like this: function queryProposals(hash:string) { let result = api?.query.backgroundCouncil.proposalOf( hash,(data1:any)=>{ let injectedData = data1.toPrimitive().args.account as InjectedAccou ...

Is it possible to utilize Typescript and Browserify in tandem?

As I explore the compatibility of TypeScript and Browserify, one perplexing aspect stands out - they both utilize 'require' but for different purposes. TypeScript uses 'require' to import other TS modules, while Browserify employs it to ...

Troubleshooting: Angular Custom Elements malfunction on Firefox, Microsoft Edge, and Internet Explorer

Experimented with the Angular Elements Demo After downloading, installing, and building the demo locally. Implemented the following code: <!doctype html> <html lang="en> <head> <meta charset="utf-8> <title>Angular Eleme ...

Error: Observable<any> cannot be converted to type Observable<number> due to a piping issue

What causes the type error to be thrown when using interval(500) in the code snippet below? const source = timer(0, 5000); const example = source.pipe(switchMap(() => interval(500))); const subscribe = example.subscribe(val => console.log(val)); V ...

How can I exclude *.d.ts files from tslint checking?

Recently, I decided to integrate tslint into my workflow. Following the installation steps, I used the command: npm install tslint tslint-config-ms-recommended --save-dev My configuration file tslint.json now looks like this: { "extends": "tslint-co ...

Error in JavaScript: A surprise anonymous System.register call occurred

Within Visual Studio 2015, there exists a TypeScript project featuring two distinct TypeScript files: foo.ts export class Foo { bar(): string { return "hello"; } } app.ts /// <reference path="foo.ts"/> import {Foo} from './f ...

Struggling to make even the most basic example work with TypeScript and npm modules

After stumbling upon this repository that made using npm modules within a Typescript program look easy, I decided to give it a try by forking it and making some changes. My goal was to add another package to get a better understanding of the process. So, I ...

What are the steps to incorporate a 3D scene into a React website?

Can I get some advice on how to create a React web application using TypeScript? I want to be able to click a button and have it show a new page with a scene of a town. What is the best way to achieve this in my React project? I've heard about using R ...

Is it possible to modify the Angular global error handler to accept additional parameters besides just the error object?

After exploring the capabilities of https://angular.io/api/core/ErrorHandler, I have found that it allows me to override the global error handler with an error object. I appreciate how I can easily define my own global error handler and use it wherever nec ...

What is the proper way to validate a property name against its corresponding value?

Here is the structure of my User class: export class User { public id: number; //Basic information public email: string; public firstName: string; public lastName: string; //Permissions public canHangSocks: boolean; p ...

Combine two elements in an array

I am faced with a challenge in binding values from an Array. My goal is to display two values in a row, then the next two values in the following row, and so on. Unfortunately, I have been unable to achieve this using *ngFor. Any assistance would be greatl ...

The issue with dispatching actions in TypeScript when using Redux-thunk

As a beginner in TypeScript, I apologize if my question seems silly, but I'll ask anyway: I'm attempting to make an async call getUsersList(), but the issue is that it's not triggering the dispatch (it's not logging "hello"). It worked ...

Angular 2 code test coverage

Looking to calculate the code coverage of my Angular 2 code. Wondering if there are any plugins available for VS Code or WebStorm that can assist with this. My unit testing is done using Jasmine and Karma. ...

What is the best way to incorporate dynamic infographics into an ionic app?

Looking to design unique infographics for my ionic app, similar to the ones seen here: Any recommendations on tools or strategies for creating these infographics? ...

Nested function TypeScript declarations

Currently, I am attempting to define a type for my controller function in (nodejs) similar to the following export const registerUser = asyncWrap(async function(req:Request, res:Response, next:NextFunction) { res.status(200).json({ success: true}); }) ...