Setting up Typescript classes that inherit from one another with diverse properties for each subclass

I'm fairly new to Typescript and currently grappling with how to effectively manage class inheritance when base classes have distinct properties.

Essentially, I have a base class where I aim to define shared functionality and a series of subclasses with varying properties that correspond to different database models. I'm trying to determine the best approach for defining the types in this scenario.

For instance:

class BaseClass {
  static create(props) { /*... */ }
  update(props) { /*... */ }
}

type SubClassOneProps = {
  firstName: string
  lastName: string
}
class SubClassOne extends BaseClass {
  firstName!: string
  lastName!: string
}

type SubClassTwoProps = {
  streetName: string
  streetNumber: number
}
class SubClassTwo extends BaseClass {
  streetName!: string
  streetNumber!: number
}

// I want to establish typings that facilitate the following:
SubClassOne.create({firstName: "Bob", lastName: "Doe"})
SubClassTwo.create({streetName: "Sunset Blvd", streetNumber: 100})

// and apply a similar concept to the instance methods, though I may utilize Partial<> for these

Due to the unique properties in each subclass, the signatures vary slightly, even though they all consist of basic key/value pairs. I'm struggling to determine the correct typing and how to specify the properties for each subclass.

In addition, I'll need to include some metadata for each property (specifically, whether they are publicly accessible), and create an instance method that can output the public properties to a JSON object. However, I'll tackle that issue separately at a later time.

Any advice or guidance on this matter would be greatly appreciated!

Answer №1

The main issue lies in the absence of polymorphic this types on static class members. For more details, check out microsoft/TypeScript#5863.

When dealing with instance members, the type named this can be used to reference the current subclass:

class BaseClass {
  declare test: Array<this>;
}

class SubClassOne extends BaseClass { /* snip */ }
const sc1 = new SubClassOne();
sc1.test // SubClassOne[]

class SubClassTwo extends BaseClass { /* snip */ }
const sc2 = new SubClassTwo();
sc2.test // SubClassTwo[]

However, there isn't a similar approach for static members. For static methods, a workaround involves making the method generic and connecting the method's this context to the generic type parameter using a this parameter:

class BaseClass {
  declare static test: <T extends BaseClass>(this: new () => T) => Array<T>;
}

class SubClassOne extends BaseClass { /* snip */ }
const sc1t = SubClassOne.test();
// const sc1t: SubClassOne[]

class SubClassTwo extends BaseClass { /* snip */ }
const sc2t = SubClassTwo.test();
// const sc1t: SubClassTwo[]

This methodology allows us to obtain a this type from a constructor indirectly.


To associate each subclass with its respective "props" type, it's suggested to turn the base class into a generic class where the type parameter T corresponds to the props type:

class BaseClass<T extends object> {
  update(props: T) { Object.assign(this, props) }
}

class SubClassOne extends BaseClass<SubClassOneProps> {
  firstName!: string
  lastName!: string
}
const sc1 = new SubClassOne();
sc1.update({ firstName: "Bob", lastName: "Doe" });

class SubClassTwo extends BaseClass<SubClassTwoProps> {
  streetName!: string
  streetNumber!: number
}
const sc2 = new SubClassTwo();
sc2.update({ streetName: "Sunset Blvd", streetNumber: 100 });

Now, we can define create():

class BaseClass<T extends object> {
  static create<T extends BaseClass<any>>(
    this: { new(): T },
    props: T extends BaseClass<infer U> ? U : never
  ) { const ret = new this(); ret.update(props); return ret; }
}

For create(), the this context must be a zero-argument constructor producing an instance of the generic type T, and conditional type inference is used to infer the props type. Essentially, if T is BaseClass<X> for some X, then

T extends BaseClass<infer U> ? U : never
evaluates to X.

Testing it out:

const sc1 = SubClassOne.create({ firstName: "Bob", lastName: "Doe" });
// sc1: SubClassOne
console.log(sc1.firstName.toUpperCase()) // "BOB"

const sc2 = SubClassTwo.create({ streetName: "Sunset Blvd", streetNumber: 100 });
// sc2: SubClassTwo
console.log(sc2.streetNumber.toFixed(2)) // "100.00"

Everything appears to be working well!

Playground link to code

Answer №2

One efficient approach for a static function is by passing a generic to it, as demonstrated below:

class BaseClass {
  static create<T>(props: T) {
    /*... */
  }
  update(props) {
    /*... */
  }
}

type SubClassOneProps = {
  firstName: string;
  lastName: string;
};
class SubClassOne extends BaseClass {
  firstName!: string;
  lastName!: string;
}

type SubClassTwoProps = {
  streetName: string;
  streetNumber: number;
};
class SubClassTwo extends BaseClass {
  streetName!: string;
  streetNumber!: number;
}

SubClassOne.create<SubClassOneProps>({ firstName: 'Bob', lastName: 'Doe' });
SubClassTwo.create<SubClassTwoProps>({ streetName: 'Sunset Blvd', streetNumber: 100 });

In this context, T represents a generic and serves as the props type.

When invoking SubClassSmth.create({}), the generic is passed in.

If the function were not static, a cleaner method could be implemented by introducing the generic at the class level, eliminating the need to redefine it, as shown below:

class BaseClass<U> {
  create(props: U) {
    /*... */
  }
  update(props) {
    /*... */
  }
}

type SubClassOneProps = {
  firstName: string;
  lastName: string;
};
class SubClassOne extends BaseClass<SubClassOneProps> {
  firstName!: string;
  lastName!: string;
}

type SubClassTwoProps = {
  streetName: string;
  streetNumber: number;
};
class SubClassTwo extends BaseClass<SubClassTwoProps> {
  streetName!: string;
  streetNumber!: number;
}

const subClassOneFactory = new SubClassOne();
const subClassTwoFactory = new SubClassTwo();

subClassOneFactory.create({ firstName: 'Bob', lastName: 'Doe' });
subClassTwoFactory.create({ streetName: 'Sunset Blvd', streetNumber: 100 });

For more insights on the limitations of using class-level generics with static functions, refer to Calling a static function on a generic class in TypeScript

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

Implement Angular backend API on Azure Cloud Platform

I successfully created a backend API that connects to SQL and is hosted on my Azure account. However, I am unsure of the steps needed to deploy this API on Azure and make it accessible so that I can connect my Angular app to its URL instead of using loca ...

Retrieve the property of a Typescript object using a template argument

I am looking to develop a Typescript Collection class that can locate items by field. Here is an example of what I have in mind: class Collection<T, K keyof T> { private _items: T[]; public isItemInCollection(item: T) { return _item ...

Using Angular: A guide to setting individual values for select dropdowns with form controls

I am working on a project that involves organizing food items into categories. Each item has a corresponding table entry, with a field indicating which category it belongs to. The category is represented by a Guid but displayed in a user-friendly format. C ...

React Native is throwing an error message saying that the Component cannot be used as a JSX component. It mentions that the Type '{}' is not assignable to type 'ReactNode'

Recently, I initiated a new project and encountered errors while working with various packages such as React Native Reanimated and React Navigation Stack. Below is my package.json configuration: { "name": "foodmatch", "version ...

Navigating to a component route in Angular

I need to retrieve the routing URL of a component that is different from the current URL and save it in a service. For example, if my component is called parentComponent and contains another component called childComponent, then the URL of the child compon ...

Using ngFor directive to iterate through nested objects in Angular

Receiving data from the server: { "12312412": { "id": "12312412", "something": { "54332": { "id": "54332", "nextNode": { "65474&q ...

The combination of a reactive form and the latest object may result in a potential null or undefined

Is it possible to update a FormArray based on the values of two other controls? After thorough checks, TypeScript is indicating issues with 'st' and 'sp'. The object is potentially null. Can someone identify the errors in this code ...

Using Webpack 4 and React Router, when trying to navigate to a sub path,

I'm currently working on setting up a router for my page, but I've encountered a problem. import * as React from 'react'; import {Route, Router, Switch, Redirect} from 'react-router-dom'; import { createBrowserHistory } from ...

Invoke an RxJs observable to handle errors and retry the process

I have an observable called submit$ that submits a form. If this observable encounters an error with status code 403, it means the user is not authorized and needs to log in first. Is there a way to automatically trigger another observable when a specific ...

How can I dynamically replace a specific element inside a .map() function with a custom component when the state updates, without replacing all elements at once?

I'm currently developing a task management application and I need to enhance the user experience by implementing a feature that allows users to update specific tasks. When a user clicks on the update button for a particular task, it should replace tha ...

Sort and incorporate elements by multiple strings present in an array

Looking to filter and include strings in an array by matching them with items in another array? Here is a sample code snippet that filters based on a single string: const filteredString = `${this.filter}`.toLowerCase(); this.filteredCampaigns = this. ...

In React-Native, implement a function that updates one state based on changes in another state

I need to trigger a function when a specific state changes. However, I encountered the error 'maximum update depth reached'. This seems illogical as the function should only respond to changes from stateA to update stateB. I attempted using setSt ...

Avoiding the use of destructuring for undefined values in JavaScript can be achieved by implementing

Upon receiving the response registryReportSettings from the server: this.getRegistrySettings(registry.Id).subscribe((registryReportSettings: { extended: ReportPropertiesRequest }) => { const { objectProperties, reportProperties, textProperties } = reg ...

Exploring Cypress: Leveraging the Power of Variable Assignment

Recently, I encountered an issue while working with a variable in a Cypress each loop. Despite incrementing the variable within the loop, it resets to zero once outside of it. Can someone shed light on why this happens and suggest a solution? Thank you. p ...

Substitute this.bindMethod for function component

I have a class component that is structured like this: interface MyProps { addingCoord: any resetCoords: any } interface MyState { x: any y: any } class DrawerOld extends React.Component<MyProps, MyState> { width: number height: number ...

Does moment/moment-timezone have a feature that allows for the conversion of a timezone name into a more easily comprehendible format?

Consider this example project where a timezone name needs to be converted to a more readable format. For instance: input: America/Los_Angeles output: America Los Angeles While "America/Los_Angeles" may seem human-readable, the requirement is to convert ...

RouterModule is a crucial external component that is essential for integrating

If I have a very simple component that is part of an Angular component library, it might look like this: mycomponent.module.html <div> <a routerLink="/"> </div> mycomponent.component.ts import { Component, OnInit, Input } from &a ...

Tips for solving a deliberate circular dependency in an angular provider

If the existing injection token for this provider is available, I want to use it. Otherwise, I will use the specified provider. Below is the code snippet: providers: [ { provide: DesignerRecoveryComponentStore, useFacto ...

Determine the category of a nested key within a different type

I'm currently utilizing graphql-codegen which generates types in the following structure: type Price = { onetime: number; monthtly: number; }; type CarModel = { price: Price; name: string; }; type Car = { model: CarModel; someOtherAttri ...

Update the style dynamically in Angular using an ngFor directive and an array of data

Is there a way to dynamically set the width using data from an array in Angular? The usual approach doesn't seem to work. How can I solve this issue? <div *ngFor="let item of products"> <div [style.width.px]="item.size" class="Holiday"&g ...