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

Why are my question documents in MongoDB collection empty when using the save() function in Node.js?

Just starting out with MongoDB and have some basic Node knowledge. I'm following an online tutorial for Node, Express, and MongoDB. Currently, I have code that successfully connects to a remote cluster and inserts a document into a collection. However ...

Typescript: Why Lines Are Not Rendering on Canvas When Using a For-Loop

What started out as a fun project to create a graphing utility quickly turned into a serious endeavor... My goal was simple - to create a line graph. Despite my efforts, attempting to use a for-loop in my TypeScript project resulted in no output. In the ...

How Angular pulls information from a JSON file using index identifiers

I am struggling to access the user item view from a json array called dealerLst. The complexity of the json is causing issues for me in accessing multiple users. Can someone guide me on how to access all children using angular or typescript? Additionally, ...

Exporting several functions within a TypeScript package is advantageous for allowing greater flexibility

Currently, I am in the process of developing an npm package using Typescript that includes a variety of functions. Right now, all the functions are being imported into a file called index.ts and then re-exported immediately: import { functionA, functionB ...

When creating an async function, the type of return value must be the universal Promise<T> type

https://i.stack.imgur.com/MhNuX.png Can you explain why TSlint continues to show the error message "The return type of an async function or method must be the global Promise type"? I'm confused about what the issue might be. UPDATE: https://i.stac ...

Edit the CSS styles within a webview

When loading the page in NativeScript using web viewing, I encountered a need to hide certain elements on the page. What is the best way to apply CSS styles to HTML elements in this scenario? Are there any alternatives that could be considered? I have been ...

Express js is not returning a value from a recursive function?

I've been working on an ecommerce website where I created a mongoose model for all categories. However, the challenge arises when dealing with subcategories that require a parent id in the database. When a request is made, all categories are retrieved ...

"Handsontable organizes information pulled directly from the backend database using JSON/AJAX

In order to implement the Handsontable column sorting and direction indicators, I would like to create a mechanism that sends sort requests to my database and displays the corresponding results. Although the Handsontable sort plugin is effective in allowi ...

Execute a selector on child elements using cheerio

I am struggling to apply selectors to elements in cheerio (version 1.0.0-rc.3). Attempting to use find() results in an error. const xmlText = ` <table> <tr><td>Foo</td><td/></tr> <tr><td>1,2,3</td> ...

Learn how to define an object with string keys and MUI SX prop types as values when typing in programming

I want to create a comprehensive collection of all MUI(v5) sx properties outside of the component. Here is an example: const styles = { // The way to declare this variable? sectionOne: { // What type should be assigned here for SXProps<Theme>? } ...

After the transition from Angular 8 to Angular 9, an issue arose with the node_modules/@zerohouse/router-tab/zerohouse-router-tab.d.ts file, as it was not declared

Error Image package.json { "name": "client", "version": "0.0.0", "license": "MIT", "scripts": { "ng": "ng", "serveapp": "ng serve ...

declare wrong TypeScript type in external library

I am currently using winston 3.0 in combination with the @types/winston types. Unfortunately, it seems that these types are not completely compatible, leading to an error in the types that I am unsure how to rectify. Below is the code snippet in question: ...

Enforce the retrieval of all fields, even those that have been overridden

I have a query that utilizes mongoose and involves a select operation. The model's schema is configured to exclude two specific fields, like so: contents: { type: String select: false }, password: { type: String select: false } Howev ...

Library types for TypeScript declaration merging

Is there a way to "extend" interfaces through declaration merging that were originally declared in a TypeScript library file? Specifically, I am trying to extend the HTMLCanvasElement interface from the built-in TypeScript library lib.dom. While I underst ...

What is the best way to change the name of an imported variable currently named `await` to avoid conflicting with other variables?

Here is the given code snippet: import * as fs from 'fs'; import {promises as fsPromises} from 'fs'; // ... // Reading the file without encoding to access raw buffer. const { bytesRead, buffer as fileBuffer } = await fsPromises.read( ...

The module "<file path>/react-redux" does not contain an exported member named "Dispatch"

Currently, I am in the process of following a TypeScript-React-Starter tutorial and have encountered an issue while wrapping a component into a container named Hello.tsx. Specifically, at line 4, the code snippet is as follows: import {connect, Dispatch} ...

The parameter label is being detected as having an any type, as specified in the Binding element 'label'

Currently, I am referencing an example code snippet from react-hook-form. However, upon implementation, I encounter the following error: (parameter) label: any Binding element 'label' implicitly has an 'any' type.ts(7031) The example c ...

Is there a way to view Deno's transpiled JavaScript code while coding in TypeScript?

As I dive into Typescript with Deno, I am curious about how to view the JavaScript result. Are there any command line options that I may have overlooked in the documentation? P.S. I understand that Deno does not require a compilation step, but ultimately ...

Exploring the concept of inheritance and nested views within AngularJS

I've encountered a challenge while setting up nested views in AngularJS. Utilizing the ui-router library has been beneficial, but I'm facing issues with separate controllers for each view without proper inheritance between them. This results in h ...

What is the best way to create a universal limitation for a larger collection of a discriminated union?

Is it possible to enforce that when defining a generic class Foo<X>, where X represents a discriminated union type, X must be a superset of another discriminated union Y? In my specific scenario, I am utilizing a discriminated union to differentiate ...