Updating TypeScript interface based on a generic value

I am currently working on modifying a TypeScript interface based on specific keys found in an object.

The object I receive from GraphQL has the following structure:

{
  "vamm": {
    "__typename": "Vamm",
    "stats": {
      "__typename": "VammStats",
      "fee": {
        "amount": "0.01",
        "__typename": "Amount"
      }
    }
  }
}

I have written a function that takes these GraphQL data objects and recursively searches for a key with the value of __typename equal to Amount. When a match is found, that object will be replaced with a class.

interface QueryObject {
  __typename: string
  [key: string]: unknown
}

type FormattedQueryData<Data> = unknown

const formatQueryData = <Data>(data: Data): FormattedQueryData<Data> => {
  if (typeof data === "object") {
    if ("__typename" in data) {
      const queryObject = data as unknown as QueryObject

      if (queryObject.__typename === "Amount") {
        const queryAmount = queryObject as QueryAmount

        return new Amount(queryAmount.amount)
      }
    }

    const accumulator: Record<string, unknown> = {}

    for (const key in data) {
      accumulator[key] = formatQueryData(data[key])
    }

    return accumulator
  }

  return data
}

This function will output an object with the same structure, except for the fee property, which will no longer look like this:

{
  "fee": {
    "amount": "0.01",
    "__typename": "Amount"
  }
}

Instead, it will be transformed into:

{
  "fee": Amount // class
}

Is there a way to update the FormattedQueryData type to reflect the returned object?

Check out this TypeScript playground for more details.

Answer №1

To tackle this issue, I propose creating FormattedQueryData<T> as a recursive conditional type that transforms

{amount: string, __typename: string}
into Amount, and otherwise utilizes mapping to process each object property via FormattedQueryData<T>. Here's how it would look:

type FormattedQueryData<T> = T extends { amount: string, __typename: string } ?
  Amount : { [K in keyof T]: FormattedQueryData<T[K]> };

It's worth noting that mappings of the form {[K in keyof T]: ...} do not modify primitive types like T, meaning if T is string, the result will simply be string.

Let's apply this to your Input:

type FormattedInput = FormattedQueryData<Input>;
/* type FormattedInput = {
    vamm: {
        __typename: string;
        stats: {
            __typename: string;
            fee: Amount;
        };
    };
} */

This matches your ExpectedOutput type. Therefore, we can implement this without any issues:

const formattedData: ExpectedOutput = formatQueryData<Input>(graphqlData); // works fine

In fact, specifying the type for Input is redundant because it can be inferred automatically:

const formattedData: ExpectedOutput = formatQueryData(graphqlData); // still works perfectly

For a hands-on demonstration and testing, you can visit this Playground link

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

Angular 4 scatter chart with multiple data points using Google charts

I'm currently developing a scatter graph in Angular 4 using ng2-google-charts from https://www.npmjs.com/package/ng2-google-charts It seems like this is essentially a wrapper for Google Chart Service. The graph looks great with a few values, but whe ...

Is it possible to locate a Typescript class only when there are no references to its properties?

Currently, I am utilizing TypeScript 2.0 within VSCode and although the highlighted errors are being confirmed by the TypeScript compiler, they all point to a module that I am importing: import * as els from 'elasticsearch'; The module elastics ...

Adonisjs latest version (v5) model creation command malfunctioning

Using Adonisjs v5 The controller command works fine with: node ace make:controller Posts However, the new model creation command is not working: node ace:make model Post When running the make model command, an error occurs: An error message stating &ap ...

A guide on creating a LoopBack 4 REST API to fetch data from a MySQL database

I am currently diving into the world of Loopback 4. I have successfully created a model, repository, and datasource in connection with MySQL. This has enabled me to retrieve results from http://127.0.0.1:3000/myapi/{id}. In my initial setup, obtaining dat ...

Troubleshooting TypeScript errors in a personalized Material UI 5 theme

In our codebase, we utilize a palette.ts file to store all color properties within the palette variable. This file is then imported into themeProvider.tsx and used accordingly. However, we are encountering a typescript error related to custom properties as ...

A guide on manipulating queryParams within the current route in angular 2 without triggering a page reload

I am currently dealing with the following route: http://localhost:96/#/pages/settings/devices I am looking to add queryParams={name:realTime} to this existing route, like so: http://localhost:96/#/pages/settings/devices?name=realTime Can I manipulate ...

Error: The variable _ is undefined when trying to use the .map() function on an array

While working on my project, I encountered a "ReferenceError: _ is not defined" when using the .map function in this code snippet: arr.map(async (elem) => { ... }); I couldn't find any explicit mention of "_" in my code. The error trace pointed me ...

Discovering the file system with window.resolveLocalFileSystemURL in Typescript for Ionic 3

After reviewing the documentation found on this link for the File plugin, I came across a paragraph that discusses how to add data to a log file. See the example code below: window.resolveLocalFileSystemURL(cordova.file.dataDirectory, function (dirEntry) ...

What are the benefits of using default ES module properties for exporting/importing compared to named module properties?

Currently studying the Material UI documentation, I came across this statement: It is noted in the example above that we used: import RaisedButton from 'material-ui/RaisedButton'; instead of import {RaisedButton} from 'material-ui&apo ...

Exploring the new possibilities of Angular 5: Enhanced REST API service with paginated data and object mapping capabilities

After implementing pagination in my REST API backend, I now need to update my Angular services to accommodate the changes. Instead of simply returning an array of objects, the API will now return JSON responses structured like this: { "count": 0, ...

Ways to retrieve dictionary keys as an array in Angular

Here is an example of an Angular dictionary: { ARRAY1: [{...}, {...}, {...}] ARRAY2: [{...}, {...}, {...}] ARRAY3: [{...}, {...}] ARRAY4: [{...}] } I want to show all the keys of arrays from this dictionary on an HTML page. I attempted to do ...

An issue arises whenever I try to set up a timer

Here is the code snippet: import {Component, OnInit} from '@angular/core'; import {SimpleTimer} from 'ng2-simple-timer'; @Component({ 'selector': 'my-app', 'template': ` <p> ng2-simple-timer ...

Slim specific assigned parameter

Here is some code to analyze: type T = 'a' | "b" type M = { a: 1, b: 2 } function a(a: 'a') {} function m1(a: 1) {} function f<TT extends T>(t: TT, p: M[TT]) { switch (t) { case "a": { a(t) ...

The Angular Http Interceptor is failing to trigger a new request after refreshing the token

In my project, I implemented an HTTP interceptor that manages access token refreshing. If a user's access token expires and the request receives a 401 error, this function is designed to handle the situation by refreshing the token and re-executing ...

Troubleshooting Typescript & Express Routing Issue

I am currently following a tutorial on how to set up a simple express-typescript application. However, I am encountering some difficulties with my routes not working as expected. Despite searching for similar problems and solutions, I haven't found an ...

Understanding how to extract a specific value key from a JSON object in Typescript while utilizing Angular can greatly

I'm currently facing a challenge in Typescript with Angular where I need to retrieve a specific value from a JSON constant. While I am aware of the performance implications, I am wondering if there is a more efficient way to access this value within t ...

Node.js is throwing the error "error TS18003: Config file does not contain any inputs."

I'm encountering an error and need help fixing it. Currently, I am using Windows 11. Every time I attempt to run npm run build in the command prompt, I encounter this specific error: Error File package.json package.json File tsconfig.json "com ...

Tips for converting a JSON object to TypeScript compliant format:

I'm looking to convert a JSON response from: [ { "value": { "name": "Actions", "visible": true, "property": "actions" } }, { ...

Refresh Information Stripe

I'm currently working on implementing Stripe, and utilizing metadata in the process. Everything works smoothly until I come across a scenario where I need to update a value in the metadata to determine if a specific uuid has been used before. pay ...

Next.js 14 useEffect firing twice upon page load

Having an issue with a client component in next js that is calling an API twice at page load using useEffect. Here's the code for the client component: 'use client'; import { useState, useEffect } from 'react'; import { useInView ...