In order to ensure consistency, it is necessary for a Typescript abstract class to specify the properties of another interface that must also be

Currently facing an issue with Typescript where I'm attempting to ensure that any objects extending an abstract class must define properties from a passed-in interface as a generic. Let me illustrate this with an example:

interface RelationshipData {
   data: {
     id: string;
     type: string;
   }
}

interface Dto<A = Record<string, any>, R = Record<string, RelationshipData>> {
    attributes: A;
    relationships: R;
}

abstract class EntityModel<T extends Dto> {
   // How do I make sure that T['attributes']'s properties and values are defined in this class?
   // The following code snippet produces errors
   [key in T['attributes']: T['attributes'][key];
}

Let's dive into how the implementation would look like:

interface ProductAttributes {
   name: string;
   description: string;
}

interface ProductRelationships {
   tenant: RelationshipData;
}

interface ProductDto extends Dto<ProductAttributes, ProductRelationships> {}

export class ProductModel extends EntityModel<ProductDto> {
    /**
     * My intention is for Typescript to flag an error here if I haven't defined the following:
     *
     * name: string;
     * description: string;
     */
}

I've experimented with the above approach but unfortunately, it doesn't seem to work.

Answer №1

At the moment, TypeScript does not support programmatically creating abstract properties or using dynamic keys in classes or interfaces. Each property must be declared explicitly, and there is an ongoing feature request for this functionality on microsoft/TypeScript#98765. Until this feature is implemented, developers need to find workarounds.


One workaround mentioned in the issue is to split the abstract class into static and dynamic parts and then use an implements clause to ensure type checking of the dynamic part:

abstract class EntityModel<T extends Data> { }
type DynamicEntityModel<T extends Data> = T["attributes"]

interface UserDto extends Data<UserAttributes, UserRelationships> {}

export class UserModel // error!
//Class 'UserModel' does not implement interface 'UserAttributes'.
//Required properties 'name', 'email', 'password'
extends EntityModel<UserDto> implements DynamicEntityModel<UserDto> { 
}

Another approach suggested is to create a class factory function for EntityModel that takes an initializer function to handle the dynamic part of the model:

abstract class StaticEntityModel { }
type DynamicEntityModel<T extends Data> = T["attributes"]
type EntityModel<T extends Data> = StaticEntityModel & DynamicEntityModel<T>

function EntityModel<T extends Data>(
addedProps: () => DynamicEntityModel<T>
) {
return class extends StaticEntityModel {
  constructor() {
    super();
    Object.assign(this, addedProps())
  }
} as new () => EntityModel<T>
}

function userDtoInitializer(): DynamicEntityModel<UserDto> {
return {
  name: "John Doe",
  email: "john.doe@example.com",
  password: "secretpassword"
}
}

export class UserModel extends EntityModel(userDtoInitializer) {

}

The factory function requires a type assertion due to TypeScript's limitations in verifying initialization.

Playground link to code

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

What impact does the Host resolution modifier have on the injection process?

The concept of Hierarchical Dependency Injection in Angular, as explained in the guide here, invites us to view DI as a combined logical tree. Explaining the use of the @Host() modifier, the guide mentions that it restricts the search scope to the <#VI ...

Remove the package from the @types folder within the node_modules directory

I currently have the 'mime' library in my node_modules directory and I am looking to completely remove it from my project, along with its @types files. The reason for this is that the old mime package is not functioning correctly for me, so I wan ...

Modifying the value of a property in an object array created using the map method is ineffective

I have a collection of objects: https://i.sstatic.net/XNrcU.png Within the collection, I wished to include an additional property to the objects. To achieve this, I utilized the map function: returnArray = returnArray.map((obj) => { obj.active = "fal ...

Change the TypeScript type of the list in order to generate a type with key-value pairs

While experimenting with TypeScript type manipulation, I attempted to modify the type provided below type setOfFunctions = [{ name: 'hi', fun: () => number }, { name: 'world', fun: () => string }] in order to achi ...

Using TypeScript 4.1, React, and Material-UI, the className attribute does not support the CSSProperties type

Just starting out with Material-UI and we're utilizing the withStyles feature to style our components. Following the guidelines laid out here, I successfully created a classes object with the appropriate types. const classes = createStyles({ main ...

Activate the download upon clicking in Angular 2

One situation is the following where an icon has a click event <md-list-item *ngFor="let history of exportHistory"> <md-icon (click)="onDownloadClick(history)" md-list-avatar>file_download</md-icon> <a md-line> ...

Determine the date and time based on the number of days passed

Hey there! I have a dataset structured like this: let events = { "KOTH Airship": ["EVERY 19:00"], "KOTH Castle": ["EVERY 20:00"], Totem: ["EVERY 17:00", "EVERY 23:00"], Jum ...

determine the values of objects based on their corresponding keys

Still on the hunt for a solution to this, but haven't found an exact match yet. I've been grappling with the following code snippet: interface RowData { firstName: string; lastName: string; age: number; participate: boolean; } c ...

The entire React component is not rendering as expected when calling res.render in PugJS within Express

Seeking guidance for the following issue: I have developed a PugJS view that is rendered within an ExpressJS route. In the call to the ExpressJS function res.render, the React component is included as data inside the .render() function call.... The prob ...

Creating a flexible TypeScript function handler that accepts optional arguments depending on the function's name

I am facing a challenge with defining the function type for running helper functions that prepare database queries. Some of these functions have arguments, while others do not. TS Playground Link type PreparedQuery = { query: string; params?: string[] ...

How to handle the "subscribe doesn't exist on type void" error when trying to subscribe to data from a service?

I am currently working on an Angular 7 application and I have encountered an error stating that the property 'subscribe' does not exist on type void when trying to subscribe to data from a service. The error is displayed in the subscribe data fu ...

The names of properties in Typescript are determined by the values of the outer type properties

In my project, I have various interfaces (or types) defined as follows: export type simpleValue = string | number | boolean | Date | null; export interface Options { inline?: OptionsItem[] | unknown[]; promptField?: string; selectedValues?: unknown[ ...

Locate and refine the pipeline for converting all elements of an array into JSON format using Angular 2

I am currently working on implementing a search functionality using a custom pipe in Angular. The goal is to be able to search through all strings or columns in a received JSON or array of objects and update the table accordingly. Here is the code snippet ...

What is the syntax for using typeof with anonymous types in TypeScript?

After reading an article, I'm still trying to grasp the concept of using typeof in TypeScript for real-world applications. I understand it's related to anonymous types, but could someone provide a practical example of how it can be used? Appreci ...

Angular 2 - Troubleshooting [(ngModel)] Not Refreshing When [Value] Changes

In my code, I am successfully setting the value of an input by calculating two other ngModels. However, despite the input value updating, the ngModel itself remains unchanged. Take a look at the snippet below: <ion-item> <ion-label>Total p ...

TS2350 Enzyme Note: The 'new' keyword can only be used with a void function

Following the setup below: import * as enzyme from 'enzyme'; import * as Adapter from 'enzyme-adapter-react-16'; enzyme.configure({ adapter: new Adapter() }); When I use jest --watch, everything runs smoothly. However, when I try web ...

The expected data type was inferred as an object, but the function is returning an array

Need help with creating a type-safe function that can accept an argument representing a prop that either has a primitive value or a specific object with values of the same primitive type. Examples include t=number, t={x:number}, or t={x:number,y:number}. ...

Utilizing TypeScript's Type Inference to Simplify Function Combinations

I'm facing a challenge with what should be simple. The types aren't coming through as expected when trying to combine a couple of functions. Is there a way to have TypeScript infer the types without explicitly specifying them? import { pipe, map ...

Tips for passing data between two components in React Router and outlet without the need for higher order functions

In my project, I have developed two components: one is named <Flights/> and the other is called <FlightResults/>. The Flights component serves as a context provider for the entire application. const Flights = () => { return ( <Flig ...

Is there a way to activate decorator support while running tests using CRA 2.1?

Struggling to set up testing for a React application utilizing decorators and Typescript within Create React App v2.1.0 Aware that official support for decorators is lacking. Successfully running the application with the help of React App Rewired and @ba ...