Home > Blockchain >  Can I make two subclasses with methods returning each others' types?
Can I make two subclasses with methods returning each others' types?

Time:09-09

I have two classes with methods that return each other:

class City {
  
  (...)

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

class Person {
  
  (...)

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

I want to have a mutable and immutable version of each class. The return values would be updated so mutables return mutables and immutables return immutables, e.g.:

class MutableCity {
  
  (...)

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

  // new methods here for mutating things
}

class ImmutableCity {
  
  (...)

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

And similarly for Person.

My plan was to write a generic abstract class for both City and Person, and have the mutable and immutable classes inherit from them, allowing them to specify which type they'll return using a type arg:

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() { // extension example
        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;
    }
}





But the abstract classes both need each other as type args:

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

And I can't nest type args infinitely:

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

How can I get these types to rely on each other? Or how else can I solve the problem?

Solutions I've considered but don't work/I don't like:

  1. Instead of AbstractCity<PersonType extends AbstractPerson> I could just do AbstractCity<PersonType>. But then the type system doesn't know what methods I can call on PersonType.
  2. I could just rewrite the methods in each mutable/immutable class with different return types, but I don't want the read-only methods duplicated.

Note: the classes also have methods that return their own type, and I'm able to get that to work using the polymorphic this instead of generics.

CodePudding user response:

TypeScript allows you to write recursively bounded generics such as

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

or mutually-recursive generics like either

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 how much granularity you need in the typings. These types can sometimes be tricky to deal with, but you should at least be able to define them. In your example you could either write it like this:

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;
  }
}

Or possibly like this:

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;
  }
}

which both compile as-is, although again, you might run into other bootstapping issues... especially for anything you actually want to be immutable, but I'll consider that out of scope for the question as asked.

Playground link to code

  • Related