Is it possible to create two subclasses where the methods return the types of each other?

I am faced with a situation where I have two classes that rely on each other's methods:

class City {
  
  (...)

  mayor(): Person {
    return this.people[0];
  }
}

class Person {
  
  (...)

  birthCity(): City {
    return this.cities.birth;
  }
}

The challenge now is to create both mutable and immutable versions of these classes, where mutables return mutables and immutables return immutables, as shown below:

class MutableCity {
  
  (...)

  mayor(): MutablePerson {
    return this.people[0];
  }

  // additional methods for mutations
}

class ImmutableCity {
  
  (...)

  mayor(): ImmutablePerson {
    return this.people[0];
  }
}

This same concept applies to the Person class as well.

My initial plan was to implement a generic abstract class for both City and Person, where the mutable and immutable classes could inherit from and specify the return types using a type argument:

class AbstractCity<PersonType extends AbstractPerson> {
  
    readonly people: PersonType[];

    constructor(startingPeople: PersonType[]) {
        this.people = startingPeople;
    }

    mayor(): PersonType {
        return this.people[0];
    }
}

class ImmutableCity extends AbstractCity<ImmutablePerson> {}

class MutableCity extends AbstractCity<MutablePerson> {
    electNewbornInfant() { // example of additional method
        this.people.unshift(
            new MutablePerson(this)
        );
    }
}

class AbstractPerson<CityType extends AbstractCity> {
  
    readonly cities: {
        birth: CityType,
        favorite?: CityType,
    };

    constructor(birthCity: CityType) {
        this.cities = {
            birth: birthCity
        };
    }

    birthCity(): CityType {
        return this.cities.birth;
    }
}

class ImmutablePerson extends AbstractPerson<ImmutableCity> {}

class MutablePerson extends AbstractPerson<MutableCity> {
    chooseFavorite(favoriteCity: MutableCity) {
        this.cities.favorite = favoriteCity;
    }
}





However, the abstract classes need each other as type arguments, creating a challenge:

Generic type 'AbstractCity<PersonType>' requires 1 type argument(s).ts(2314)
Generic type 'AbstractPerson<CityType>' requires 1 type argument(s).ts(2314)

Nesting type arguments infinitely is not a viable solution:

class AbstractCity<
  PersonType extends AbstractPerson<
    CityType extends AbstractCity<
      PersonType extends AbstractPerson<
        CityType extends AbstractCity<
          (etc.)
        >
      >
    >
  >
>

How can I resolve this issue with the types depending on each other? Or is there an alternative solution?

Some solutions that have been considered but were not suitable:

  1. Using AbstractCity<PersonType> instead of
    AbstractCity<PersonType extends AbstractPerson>
    removes information about the methods that can be called on PersonType.
  2. Rewriting methods in each mutable/immutable class with different return types would cause duplication of read-only methods.

It should be noted that the classes also have methods that return their own type, which has been achieved using the polymorphic this instead of generics.

Answer №1

TypeScript provides the capability to define recursively bounded generics like so

class Foo<F extends Foo<F>> { f?: F }

or create mutually-recursive generics such as

class Foo<F extends Foo<F, B>, B extends Bar<F, B>> { f?: F; b?: B }
class Bar<F extends Foo<F, B>, B extends Bar<F, B>> { f?: F; b?: B }

or

class Foo<B extends Bar<Foo<B>>> { f?: Foo<B>; b?: B; }
class Bar<F extends Foo<Bar<F>>> { f?: F; b?: Bar<F>; }

depending on the level of detail you require in the typings. Working with these types can be challenging at times, but they are definable. For instance, you could implement them as shown below:

class AbstractCity<P extends AbstractPerson<AbstractCity<P>>> {
  readonly people: P[];

  constructor(startingPeople: P[]) {
    this.people = startingPeople;
  }

  mayor(): P {
    return this.people[0];
  }
}

class ImmutableCity extends AbstractCity<ImmutablePerson> { }
class MutableCity extends AbstractCity<MutablePerson> {
  electNewbornInfant() {
    this.people.unshift(new MutablePerson(this));
  }
}

class AbstractPerson<C extends AbstractCity<AbstractPerson<C>>> {
  readonly cities: {
    birth: C;
    favorite?: C;
  };

  constructor(birthCity: C) {
    this.cities = {
      birth: birthCity
    };
  }

  birthCity(): C {
    return this.cities.birth;
  }
}

class ImmutablePerson extends AbstractPerson<ImmutableCity> { }
class MutablePerson extends AbstractPerson<MutableCity> {
  chooseFavorite(favoriteCity: MutableCity) {
    this.cities.favorite = favoriteCity;
  }
}

Alternatively, you could structure them like:

class AbstractCity<C extends AbstractCity<C, P>, P extends AbstractPerson<C, P>> {
  readonly people: P[];

  constructor(startingPeople: P[]) {
    this.people = startingPeople;
  }

  mayor(): P {
    return this.people[0];
  }
}

class ImmutableCity extends AbstractCity<ImmutableCity, ImmutablePerson> { }
class MutableCity extends AbstractCity<MutableCity, MutablePerson> {
  electNewbornInfant() {
    this.people.unshift(new MutablePerson(this));
  }
}

class AbstractPerson<C extends AbstractCity<C, P>, P extends AbstractPerson<C, P>> {
  readonly cities: {
    birth: C;
    favorite?: C;
  };

  constructor(birthCity: C) {
    this.cities = {
      birth: birthCity
    };
  }

  birthCity(): C {
    return this.cities.birth;
  }
}

class ImmutablePerson extends AbstractPerson<ImmutableCity, ImmutablePerson> { }
class MutablePerson extends AbstractPerson<MutableCity, MutablePerson> {
  chooseFavorite(favoriteCity: MutableCity) {
    this.cities.favorite = favoriteCity;
  }
}

Both implementations compile as expected, though you may encounter some bootstrapping challenges, particularly with anything you want to be immutable. However, that may be beyond the scope of the original question.

Check out the playground link for the 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

Combining Arrays of Items in Typescript

I am dealing with an array in Angular 14 that looks like this: [ { "parentItem": "WBP258R", "childItem": "WBP258R", "columnName": "Color", "oldValue": ...

Utilizing Vue class-style components for creating a recursive component

I'm currently working with a class-style component using the vue-property-decorator plugin. I want to create a recursive component that can use itself within its own structure. Here's a snippet of my code: <template> <ul> <li& ...

Angular 6 Error: Failed to parse template. Unable to establish a connection with 'routerLink' as it is not recognized as a valid property of 'a'

During app testing with npm test An error is encountered : Failed: Template parse errors: Can't bind to 'routerLink' since it isn't a known property of 'a'. (" <nav> <ul> <li><a class=" ...

Error in Angular 2: Component unable to locate imported module

I'm facing an issue where a module I want to use in my application cannot be found. The error message I receive is: GET http://product-admin.dev/node_modules/angular2-toaster/ 404 (Not Found) The module was installed via NPM and its Github reposito ...

Issue with Angular 6 auth guard causing logged-in users to remain on a blank page

I came across this answer and am tweaking it to work with my authentication system: Angular 6 AuthGuard However, I'm facing an issue where after successful authentication, instead of redirecting to the specified URL, the auth guard leads to a blank p ...

Group data by two fields with distinct values in MongoDB

I have developed a Typescript Node.js application and I am looking to organize documents by two fields, "one_id" and "two_id", based on a specific "one_id" value. Below is the data within my collection: { "_id":"5a8b2953007a1922f00124fd", "one_id ...

It takes a brief moment for CSS to fully load and render after a webpage has been loaded

For some reason, CSS is not rendering properly when I load a webpage that was created using React, Next.js, Material UI, and Styled-components. The website is not server-side rendered, but this issue seems similar to what's described here You can see ...

Why did my compilation process fail to include the style files despite compiling all other files successfully?

As English is not my first language, I kindly ask for your understanding with any typing mistakes. I have created a workspace with the image depicted here; Afterwards, I executed "tsc -p ." to compile my files; You can view the generated files here Unf ...

Looking to modify the height and width of an image when it is hovered over using inline CSS

My current project involves working with a dynamic template where the HTML code is generated from the back-end using TypeScript. I am trying to implement inline CSS on hover, but despite having written the necessary code, it does not seem to work as intend ...

Ensuring the type of a specific key in an object

Seeking a more stringent approach regarding object keys in TypeScript. type UnionType = '1' | '2' | '3' type TypeGuardedObject = { [key in UnionType]: number } const exampleObject: TypeGuardedObject = { '1': 1, ...

The ultimate guide to loading multiple YAML files simultaneously in JavaScript

A Ruby script was created to split a large YAML file named travel.yaml, which includes a list of country keys and information, into individual files for each country. data = YAML.load(File.read('./src/constants/travel.yaml')) data.fetch('co ...

Sending a CSS class to an Angular library

In my development process, I am currently working on creating a library using Angular CDK specifically for custom modals. One feature I want to implement is the ability for applications using the library to pass a CSS class name along with other modal conf ...

Error Passing Data to Child Component in Typescript on Next.JS 14 Compilation

Although there are many similar questions, I am struggling to make my code work properly. I recently started learning typescript and am having trouble passing data to the child component. While the code runs fine in development (no errors or warnings), i ...

Guide on deactivating the div in angular using ngClass based on a boolean value

displayData = [ { status: 'CLOSED', ack: false }, { status: 'ESCALATED', ack: false }, { status: 'ACK', ack: false }, { status: 'ACK', ack: true }, { status: 'NEW', ack ...

What is the primary purpose of the index.d.ts file in Typescript?

There are some projects that include all types declarations within the index.d.ts file. This eliminates the need for programmers to explicitly import types from other files. import { TheType } from './somefile.ts' Is this the proper way to use ...

Troubleshooting the issue with the AWS CodeBuild SAM esbuild integration not functioning

I currently have a Lambda's API Gateway repository in CodeCommit that I successfully build using esbuild with CLI (SAM BUILD and then SAM DEPLOY). Now, I am looking to streamline the building process by integrating it with CodePipeline. I started exp ...

Which library do you typically employ for converting .mov files to mp4 format within a React application using Typescript?

As a web programming student, I have encountered a question during my project work. In our current project, users have the ability to upload images and videos. Interestingly, while videos can be uploaded successfully on Android devices, they seem to face ...

Having trouble accessing functions within the webpack bundle

As someone new to the world of JS library development, I have embarked on a journey to achieve the following goals: Creating a library with TypeScript Generating a bundle using webpack5 Publishing the library to npm Utilizing the library in other projects ...

Issue encountered: NPM error, unable to find solution for resolving dependency and addressing conflicting peer dependency

I am facing difficulties deploying my project on netlify due to NPM errors. Below are the dependencies: "dependencies": { "@angular/animations": "~15.1.1", ... (list of dependencies continues) ...

How to resolve logic issues encountered during Jasmine testing with describe()?

I am encountering an issue while testing the following code snippet: https://i.sstatic.net/ImwLs.png dateUtility.tests.ts: import { checkDayTime } from "./dateUtility"; describe("utilities/dateUtility", () => { describe("ch ...