Creating a TypeScript wrapper class that can be serialized and deserialized efficiently

Working on a utility library designed to enhance the functionality of a complex API model that receives JSON-parsed objects. These objects come with specific structure guarantees, as outlined below:

// Defined message structure:
interface InputBox {
  top: number;
  height: number;
  left: number;
  width: number;
}
interface InputObj {
  box: InputBox
}

// Example of user code processing the data:
const inputObj: InputObj = JSON.parse(
  '{ "box": { "top": 0, "height": 10, "left": 1, "width": 2, "color": "red" } }'
);

The objective is to create a modified version of these objects that:

  • Includes additional utility functions and properties
  • Stringifies into JSON that is compatible with the original input
  • Maintains consistent behavior when treated as mutable
  • Preserves any unexpected attributes if possible

For instance, the user's code may appear as follows:

// Both in-place modification or new object creation are acceptable:
const easyObj = myCoolLibrary(inputObj);

easyObj.box.top = 5;
console.log(easyObj.box.getBottom());  // '15'

JSON.stringify(easyObj);
// -> { "box": { "top": 5, "height": 10, "left": 1, "width": 2, "color": "red" } }
// (Note that box.color remains present, although not part of the initial interface)

After exploring various options, it appears that:

  • An in-place solution using Object.setPrototypeOf to directly overwrite prototypes in the input could be effective, but compatibility with older browsers might require polyfilling.
  • Creating new objects (via classes/prototypes) and transferring properties using Object.assign could also work since the input objects are simple JSON-able Objects... However, this method may still need polyfilling for IE, but could be more standardized?

The challenge lies in implementing these methods while ensuring TypeScript compatibility:

class MyCoolBox implements InputBox {
  constructor(box: InputBox) {
    Object.assign(this, box);
  }

  getBottom() {
    return this.top + this.height;
  }
}
// > Class 'MyCoolBox' incorrectly implements interface 'InputBox'.
// (and does not recognize properties like .top, .height)

Object.setPrototypeOf(inputObj.box, MyCoolBox);
inputObj.box.getBottom();
// > Property 'getBottom' does not exist on type 'InputBox'
// Fails to acknowledge the change in interface.

Is there a practical approach that TypeScript would comprehend? It seems reasonable to want to augment a JSON-parsed object (with a defined interface) with additional methods!

Answer №1

Initially, it appears that there is a misconception about the functioning of the implements keyword. While the properties are acknowledged by the compiler, you specified that your class implements those properties without actually implementing them. Although using Object.assign in the constructor may achieve the desired outcome at runtime, the compiler remains unaware of this.

If you prefer not to explicitly list every potential property of the InputBox within the class, consider leveraging the fact that classes possess interfaces with matching names bound to them. By making the MyCoolBox interface a subtype of the interface you intend to extend (i.e., the InputBox):

interface MyCoolBox extends InputBox { }

class MyCoolBox {
  constructor(box: InputBox) {
    Object.assign(this, box);
  }

  getBottom() {
    return this.top + this.height;
  }
}

Secondly, the anticipated behavior of setPrototypeOf as a type guard does not align with its actual definition:

ObjectConstructor.setPrototypeOf(o: any, proto: object | null): any

Modifying the prototype via setPrototypeOf does not alter the way the compiler interprets the structure of the box object; it still remains an InputBox. Additionally, JavaScript classes primarily function as syntactic sugar for functions plus prototype-based inheritance.

Assigning the prototype of box to MyCoolBox will lead to runtime failure when attempting to call the non-static method getBottom since only the static aspect of the class is set this way. To address this, aim to adjust the prototype of MyCoolBox - thereby configuring the instance properties:

const myCoolLibrary = <T extends InputObj>(input: T) => {
  Object.setPrototypeOf(input.box, MyCoolBox.prototype); //note the prototype
  return input as T & { box: MyCoolBox };
};

const enhanced = myCoolLibrary(inputObj);
const bottom = enhanced.box.getBottom(); //OK
console.log(bottom); //10

Finally, following the example above, specify the output type to be of the enhanced variety either through a simple type assertion (as T & { box: MyCoolBox }) or allow the compiler to infer the upgraded type:

{
  const myCoolLibrary = (input: InputObj) => {
    return {
      ...input,
      box: new MyCoolBox( input.box )
    }
  };

  const enhanced = myCoolLibrary(inputObj);
  const bottom = enhanced.box.getBottom(); //OK
  console.log(bottom); //10
}

Playground

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

Is it considered a best practice to utilize JavaScript for positioning elements on a

I recently started learning JavaScript and jQuery, and I've been using them to position elements on my website based on screen and window size. It's been really helpful, but I'm starting to wonder if it's a good practice since it makes ...

Utilizing Zoomdata data in conjunction with echarts index.js to create a dynamic stacked line chart

I am currently working on integrating Zoomdata with an echarts javascript chart to visualize data from 20 different computers in a stacked line chart format. While I can manually code this setup, I am looking for a way to dynamically link the data from Zoo ...

Exploring the power of promises in the JavaScript event loop

Just when I thought I had a solid understanding of how the event loop operates in JavaScript, I encountered a perplexing issue. If this is not new to you, I would greatly appreciate an explanation. Here's an example of the code that has left me scratc ...

Angular's table data display feature is unfortunately lacking

Below is a simple HTML code snippet: <div class="dialogs"> <div id="wrapper" > <p>{{createTestingConstant()}}</p> <ng-container *ngFor="let one of contacts"> <p>{{one ...

What are the best practices for effectively utilizing the nodejs Stream API?

I am currently working on an application that reads input from various sources, including files, sockets, and other parts of the same program that produce buffers or strings. While I have successfully handled sockets and files using node's Stream API ...

Preventing Angular $rootElement.on('click') from affecting ReactJS anchor tag interactions

Running both AngularJS and ReactJS on the same page has caused an issue for me. Whenever I click on a ReactJS <a> tag, Angular's $rootElement.on('click) event is triggered and the page redirects. I need to perform some functionality in Re ...

Controlling the visibility of components or elements in Angular through input modifications

Is there a more efficient way to handle button disabling and enabling based on email validation in Angular? I already have form controls set up, but want to make the process cleaner. The goal is to disable the "Get Started" button by default if the email a ...

Retrieve a boolean value through an Ajax call from a C# function using T4MVC

I have a search bar on my website with the following code: <form onsubmit="return IsValidCustomer()"> <input class=" sb-search-input" placeholder="Search for a customer..." type="text" value="" name="search" id="search"> <input cl ...

What could be the reason for the "begin" script failing to execute?

I have a simple node and express application that works as expected, but I encounter an issue when trying to run the server from the script. When my script is set to "start", running the command simply opens my cmd. However, if my script is set to any oth ...

Customize bullet list icons to adjust in size based on content using css

In my CMS project, the CMS team has a special requirement regarding unordered and ordered lists. They want the size of the bullet list icon to adjust according to the text content within the list. The image below shows the default design of a list item: ...

What is the best way to store information in my express server using Angular?

After spending several hours on this issue, I am feeling stuck. Initially dealing with a CORS problem, I managed to solve it. However, my goal is to utilize $resource without creating a custom post method. My API follows RESTful standards where POST /artis ...

Using JMeter's Json Extractor to Parse Data from Multiple HTTP Requests

After configuring a ForEach Controller to run several HTTP requests, my goal is to extract JSON values from the response bodies of each request. However, when attempting to use a JSON Extractor PostProcessor on the HTTP Request, I am only able to retriev ...

Add values to an array using the reduce function

My array consists of multiple objects: const items = [{ search_type: 'environment', search_code: 'TBA_ENVIRONMENT00002', asset_code: 'ASSET00002' }, { search_type: 'job', search_code: 'TBA_JOB0000 ...

JavaScript maintain a variable that holds onto nodes even after they have been removed

Recently, I encountered a seemingly simple issue with JavaScript that has me stumped. In my code, I have a variable that stores nodes with a specific class. My goal is to remove these nodes from the DOM while still retaining them in the variable. However, ...

Employ json_populate_recordset in a manner that ignores letter case

Can PostgreSQL (9.6) use json_populate_recordset to compare table column names/json keys in a case-insensitive manner? For instance, if the code snippet below was executed: CREATE TABLE foo (bar TEXT); SELECT * from json_populate_recordset(null::foo, &ap ...

Ensure to verify the presence of a null value within the ngFor iteration

I have a webpage where I am displaying information in buttons. These buttons show various objects from a list along with their corresponding fields (e.g. object.name, object.age, etc.). Sometimes, one of those fields is null. How can I check if a value is ...

Surprising non-synchronous operation within the computed property "vue/no-async-in-computed-properties" in Vue3

I am currently working on a Vue3 project and encountering an error when running it. Below is the complete code snippet that I am using. Can anyone assist me in resolving this issue? Thank you in advance. <script> import axios from 'axios'; ...

Click to save image using jQuery

I am trying to implement a feature where clicking on an image allows users to download it. I am using the download attribute for this purpose. <a href="http://mysite.ru/userfiles/certificate_2.png" class="download-certificate-link" data-title="certific ...

Prevent background music from being manipulated

I've implemented an audio HTML element with background music for a game: <audio class="music" src="..." loop></audio> However, I have encountered an issue where upon loading the page, I am able to control the music usi ...

Record the user actions from logging in through Facebook or OAuth and store them in the database

I have been attempting to log in with Facebook using Firebase. How can I save user information such as email and username to the database? Additionally, I am unsure of what steps to take next when users engage in activities like ordering products on my w ...