What is the best way to ensure that a class instance only receives the necessary properties?

In my code, I have a function that takes in an object called DataObject and uses certain properties from it to create instances of a class.

To determine which data object items should be assigned to which class properties, I use mappings in the form of a list of tuples. Each tuple consists of a key from the data object and a corresponding property name in the class.

interface DataObject<T> {
  [name: string]: T[keyof T];
}

// list of mappings
type MappingsList<T> = [string, keyof T][];

// function for creating class instances and assigning properties based on mappings
function AttrConstructor<T>(
  ItemClass: { new (): T },
  mappings: MappingsList<T>,
  dataObj: DataObject<T>
) {
  const instance = new ItemClass();
  mappings.forEach(([fromLabel, toLabel]) => {
    instance[toLabel] = dataObj[fromLabel];
  });

  return instance;
}

Everything works fine when dealing with one class at a time. However, issues arise when the data object contains properties and values for multiple classes.

class Class1 {
  Prop1a: string;
  Prop1b: number;
}
class Class2 {
  Prop2a: string;
  Prop2b: number;
}

declare const row: DataObject<Class1 & Class2>;

const mappings1: MappingsList<Class1> = [["prop1a", "Prop1a"]];

const makeNew1 = (row: DataObject<Class1>) =>
  AttrConstructor(Class1, mappings1, row);

const instance1 = makeNew1(row);

This results in an error:

Argument of type 'DataObject<Class1 & Class2>' is not assignable to parameter of type 'DataObject<Class1>'.
  Type 'Class1' is not assignable to type 'Class1 & Class2'.
    Type 'Class1' is not assignable to type 'Class2'.
      Property 'Prop2a' is missing in type 'Class1'.

The question is how can I indicate that having extra properties in the data object is acceptable because the AttrConstructor function will only assign relevant properties to each class?


Note: Interestingly, no errors were displayed during coding this example until the file was saved, so perhaps my tsconfig.json file could be relevant:

// tsconfig.json
{
  "include": ["src/**/*"],
  "compilerOptions": {
    "strict": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "module": "commonjs",
    "outDir": "dist",
    "pretty": true,
    "lib": ["es2015"],
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Answer №1

It seems that both DataObject<Class1> and DataObject<Class2> should technically be the same as {[k: string]: string | number}, but TypeScript is struggling to recognize this equivalence. It appears that TypeScript may not be delving deep enough into the DataObject<T> interface to understand that these types are actually identical. This could be due to a tendency for TypeScript to view DataObject<T> as "invariant in T," meaning it requires exact compatibility between T and any other type used with DataObject. Yet, in this case, DataObject<T> intentionally sheds some of its dependency on

T</code, resulting in the oversight of their structural similarity. If you encounter this issue, consider raising it on GitHub if no relevant discussion exists already.</p>

<hr>

<p>To address this challenge, one solution is to modify <code>interface DataObject<T> {...}
to type DataObject<T> = {...}. By utilizing a type alias instead of an interface, the compiler simplifies
DataObject<Class1 & Class2>
and DataObject<Class1> to {[k: string]: string | number} from the outset, enabling your code to compile correctly.

An alternative approach involves altering the definition to something like

interface DataObject<P> {[k: string]: P}
, introducing
type ValueOf<T> = T[keyof T]
, and employing
DataObject<ValueOf<Class1>>
and
DataObject<ValueOf<Class1 & Class2>>
instead. By refining how DataObject<P> relies on P, the compiler's assessment becomes more precise, albeit necessitating adjustments in your use of DataObject<T>.

If sticking with the existing interface is imperative, there are likely additional workarounds available. One potential option involves devising a function that converts DataObject<T> to DataObject<U> provided they are structurally compatible. For instance:

type Id<T> = {[K in keyof T]: T[K]}
function cast<T, U>(x: U & (Id<U> extends Id<T> ? U : never)): T {
  return x as any as T;
}
const instance1 = makeNew1(cast(row)); // possibly functional?

This method leverages conditional types, a feature introduced in TypeScript v2.8, where cast() facilitates transitioning from T to U</code if <code>U structurally extends T.


That's all the strategies I can suggest at this moment. Best of luck!

    

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

Angular2 Error: Cannot have two identifiers with the same name, 'PropertyKey' is duplicated

I am currently developing an application with angular2 using angular-cli. Unfortunately, angular-in-memory-web-api was not included by default. After some research, I manually added the line "angular-in-memory-web-api": "~0.1.5" to my ...

Protobuf.js: The Writer is not completing the operation

I have recently come across an unusual issue while incorporating Protobuf into my TypeScript frontend. My approach involves using Axios to communicate with my REST API and leveraging the protobuf.js library for handling Protobuf in the frontend. Since I am ...

Guide to automatically closing the calendar once a date has been chosen using owl-date-time

Utilizing Angular Date Time Picker to invoke owl-date-time has been functioning flawlessly. However, one issue I have encountered is that the calendar does not automatically close after selecting a date. Instead, I am required to click outside of the cal ...

Adapting imports in Typescript for seamless npm distribution

Currently, I'm facing an issue with module resolution while compiling my NPM package written in Typescript for publishing. In my project, I've been using non-relative imports to avoid the hassle of excessive ../../../. However, according to TypeS ...

Invoking a method in a derived class upon completion of asynchronous logic within the base class

Currently, I am in the process of building an Angular application. One aspect of my project involves a class that extends a base class. While this approach may not be ideal, I am curious to know what would be the best practice for BaseClass to trigger me ...

In Angular, is there a way to transform time into the format of YYYY-MM-DDThh:mm:ssTZD?

Our backend is built with Java and we are using the ISO 8601 standard for date formatting. In order to pass dates in this format, I require a method to convert the date into the specified format. In Java, we use: DateFormat iso8601 = new SimpleDateFormat( ...

Ensuring the proper typescript type for assigning a value in react-hook-form

I'm encountering an issue when trying to pass the function setValue() down to a child component. The error message I receive is: Type 'UseFormSetValue<Inputs>' is not assignable to type 'UseFormSetValue<Record<string, any> ...

Utilizing the Double Mapping Feature in React with Typescript

It seems I might be overlooking something, can someone guide me on how to properly double map in this scenario? I'm encountering an error on the second map: Property 'map' does not exist on type '{ departure: { code: string; name: strin ...

Leveraging Expose in combination with class-transformer

I have a simple objective in mind: I need to convert the name of one property on my response DTO. refund-order.response.dto.ts export class RefundOrderResponseDto { @Expose({ name: 'order_reference' }) orderReference: string; } What I w ...

Implementing Autocomplete feature in Reactjs with Ant Design Library

In my React application, I created an autocomplete feature with a list of options for the user to select from. Example in Action: Click here to see the demo List of available options: const options = [ { label: "hello", value: "1" ...

Disarrayed generic parameters in TypeScript

The title of the question may not be perfect, but it's the best I could do. Imagine a scenario where there is a function that accepts a callback along with an optional array. The callback takes an index and another optional array as parameters, and t ...

Do constructors in TypeScript automatically replace the value of `this` with the object returned when using `super(...)`?

I’m having some trouble grasping a concept from the documentation: According to ES2015, constructors that return an object will automatically replace the value of “this” for any instances where “super(…)” is called. The constructor code must ...

Minimize the amount of information retrieved and shown based on the timestamp

I am currently working on storing and retrieving the date of a user request. For creating the timestamp, I use this code: const date = firebase.firestore.FieldValue.serverTimestamp(); This is how I fetch and display the data: <tr class="tr-content" ...

What is the reason for Google Chrome extension popup HTML automatically adding background.js and content.js files?

While using webpack 5 to bundle my Google Chrome extension, I encountered an issue with the output popup HTML. It seems to include references to background.js and content.js even though I did not specify these references anywhere in the configuration file. ...

The canActivate: [AuthGuard] feature on the external router is not functioning as expected

I'm encountering an issue with my routing. I attempted to use the following code: const routes: Routes = [ { path: 'home', component: HomeComponent, canActivate: [AuthGuard], children: [ { path: 'events', component: Ev ...

Hold off until the operation finishes executing and then deliver Angular 6

Is there a way to delay the subscribe function until my logic is complete and the transform method has updated the keys object? transform(value: any, args:string) : any { let keys = []; this.http.get('src/app/enum-data/enum.json').subsc ...

Angular: Refresh mat-table with updated data array after applying filter

I have implemented a filter function in my Angular project to display only specific data in a mat-table based on the filter criteria. Within my mat-table, I am providing an array of objects to populate the table. The filtering function I have created loo ...

The term "containerName" in SymbolInformation is utilized to represent the hierarchy of

In my quest to make the code outline feature work for a custom language, I have made progress in generating symbols and displaying functions in the outline view. However, my next challenge is to display variables under the respective function in the outlin ...

There seems to be an issue with Firebase authentication on firebase-admin in node.js. Your client is being denied permission to access the URL "system.gserviceaccount.com" from the server

Issue I've been utilizing Firebase auth on my client and using firebase-admin to verify on the server. It was functioning well until I decided to migrate to a different server, which caused it to stop working. The crucial part of the error message i ...

In order to iterate through a 'IterableIterator<number>', you must either enable the '--downlevelIteration' flag or set the '--target' to 'es2015' or newer

While attempting to enhance my Slider, I encountered an error when utilizing TypeScript React within this Next.js project: "Type 'IterableIterator' can only be iterated through when using the '--downlevelIteration' flag or with a ...