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

Upgrade your development stack from angular 2 with webpack 1 to angular 6 with webpack 4

Recently, I have made the transition from Angular 2 and Webpack 1 to Angular 6 and Webpack 4. However, I am facing challenges finding the best dependencies for this new setup. Does anyone have any suggestions for the best dependencies to use with Angular ...

React-Redux: Unable to access the 'closed' property as it is undefined

Encountered a problem when using dispatch() in React-Redux. Specifically, the action below: export const fetchMetrics = () => { dispatch(fetchMetricsBegin); APIService.get('/dashboard/info/') .then((response) => { ...

React Material-UI - Mobile-friendly Masonry Layout

I'm new to working with React and I'm currently exploring how to create a Masonry image grid that is responsive for mobile devices. The goal is to have the images stack on top of each other in mobile view while maintaining the classic Masonry gri ...

Customizing MUI Themes with TypeScript: How do I inform TypeScript that the theme is provided by the provider?

Below is a modified demo code snippet extracted from Material UI documentation: function ThemeUsage() { const theme = { palette: { primary: { main: "#000", }, }, } as const; type DefaultThemeType = { theme: type ...

Enhancing data validation and converting strings to dates with Nest.js - DTO approach

Anticipating the upcoming request to adhere to the ISO 8601 standard timestamp format, something similar to "2023-12-04T15:30:00Z" (typically embedded as a string within JSON data that needs conversion to a JavaScript Date object). This is my Data Transfe ...

Issue with dynamically typed object properties in Typescript codebases

Check out this TypeScript code snippet: export type Mutation = (state: State, data: unknown) => void export type Mutations = { [key: string]: Mutation } private buildMutations(): Mutations { return { ['3']: (state, data) => ...

Typescript: The type 'T' fails to meet the requirement of being an 'object'

Ever since I installed a package along with its @types package, I've been encountering an issue with the following code: https://i.stack.imgur.com/rrRhW.png This is the error message that I'm receiving: https://i.stack.imgur.com/BfNmP.png The ...

Is there a way to convert a typescript alias path to the Jest 'moduleNameMapper' when the alias is for a specific file?

I am currently working on setting up Jest in a TypeScript project. In our tsconfig.json file, we are using path aliases like so: "baseUrl": ".", "paths": { "@eddystone-shared/*": [ "../shared/*" ], "@eddystone-firebase-helpers/*": [ "src/helpers/fire ...

Printing using *ngFor will display items in an ascending order

When attempting to display an object in markup, I am running into the issue of *ng printing it in ascending order instead of maintaining the original order. Ideally, I would like the elements to be printed as they are. You can view my code on StackBlitz ...

The Next.js website displays a favicon in Chrome, but it does not appear in Brave browser

As I work on my debut next.js website, I am configuring the favicon in index.js like this: <Head> <title>Create Next App</title> <link rel="icon" href="/favicon.ico" /> </Head> Initially, all my source ...

Enhancing HTML through Angular 7 with HTTP responses

Sorry to bother you with this question, but I could really use some help. I'm facing an issue with updating the innerHTML or text of a specific HTML div based on data from an observable. When I try to access the element's content using .innerHTM ...

The EventEmitter in Angular 8 is prohibiting the emission of an Object

Is there a way I can emit an Object instead of primitive data types? I have an object const amount = { currenty: 'USD', total: '10.25' }; that I need to emit to the parent component. export class MyChildComponent implements OnInit { ...

Update the TemplateUrl according to the URL Parameters (GET)

I've created a basic Angular code snippet (test.component.ts) that retrieves a variable from GET parameters: import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; @Component({ select ...

Issue encountered while trying to iterate through an observable: Object does not have the capability to utilize the 'forEach' property or method

I am currently following the pattern outlined in the hero.service.ts file, which can be found at this link: https://angular.io/docs/ts/latest/guide/server-communication.html The Observable documentation I referenced is available here: When examining my c ...

Defining a TypeScript interface specifically tailored for an object containing arrow functions

I encountered an issue while trying to define an interface for the structure outlined below: interface JSONRecord { [propName: string]: any; } type ReturnType = (id: string|number, field: string, record: JSONRecord) => string export const formatDicti ...

Examining Axios HttpService piping through a NestJS middleware in a unit test

A middleware function retrieves a JSON document from a microservice endpoint and appends it to the request. The good path test is successful, but I'm struggling to make the bad path test throw a ForbiddenException and stop it from invoking next(). W ...

Does anyone have experience using the useRef hook in React?

Can someone help me with this recurring issue: "Property 'value' does not exist on type 'never'" interface InputProps { name: string; icon?: ReactElement; placeholder?: string; } const Input = ({ name, icon: Icon, ...rest }: Inpu ...

How can you incorporate TypeScript's dictionary type within a Mongoose schema?

When using TypeScript, the dictionary type format is: { [key: string]: string; } However, when I try to define a custom schema in mongoose, it doesn't work as expected. const users = new Schema({ [key: string]: String, }); I also attempted t ...

What is the approach taken by this component to display its child elements?

While delving into the code of react-accessible-accordion, I found myself puzzled by the way it handles rendering its children. The snippet below is from Accordion.tsx: export default class Accordion extends React.Component<AccordionProps> { // ...

Handlebar files are not compatible with Typescript loading capabilities

I am encountering an issue with my directory structure as follows : src |- server |- myServer.ts |- views |- myView.hbs dist |- server |- myServer.js The problem lies in the fact that the dist folder does not have a views subfolder, where the J ...