Home > Blockchain >  Inheritance with type restriction in typescript
Inheritance with type restriction in typescript

Time:08-30

what I'm trying to implement:

the high level abstract class called Unit containing most part of logic shared between all his descendants. Each descendant has own implementation of 'act' method with own type to return. The top (highest level) abstract class has interface for a method act, this method can return any of types that descendant can create, so type in declaration his type very dynamic. But each descendant can work only with own type, so i don't want to copy/paste abstract method declaration, because of two reasons:

  1. It seems redundant.
  2. When in real class I implement logic based on abstract descendant of top Unit class, TS allows me to return anything top class has in his declaration. But I want TS to restrict implementation with exactly type is declared in descendant of a Unit class.

The problem

When I try to make declaration inside the descendant that is stricter than type in parent, TS saying my type is not compatible

Playground Link

Code from example:

// declare some enum of types I need for abstract classes
enum UnitActions {
  run = 'run',
  fly = 'fly',
  dive = 'dive',
}

// declare different structure of results that my abstract (not a top one) classes should return
type UnitRunResult = { distance: number; time: number };
type UnitFlyResult = { distance: number; vector: { x: number; y: number } };
type UnitSwimResult = { distance: number; depth: number };

// dynamic types for results based on enum
type UnitActionResults<T extends UnitActions> = T extends UnitActions.run
  ? UnitRunResult
  : T extends UnitActions.fly
  ? UnitFlyResult
  : T extends UnitActions.dive
  ? UnitSwimResult
  : never;

// main abstract class, contains a lot of high-level logic, omitted for readability
abstract class Unit {
  public abstract actionType: UnitActions;
  // abstract method that define only common idea
  public abstract act<T extends UnitActions>(): UnitActionResults<T>;
}

// abstract class that implements basic logic for some exactly kind of units
abstract class WoodenUnit extends Unit {
  public actionType = UnitActions.run;
  // on this level I already know that my wooden units can only Run.
  // is there any way to declare only one possible kind of returning result?
  public abstract act<T extends UnitActions>(): UnitActionResults<T>;

  // I've tried to strictly define T as UnitActions.run - it doesn't work
  // public abstract act<T extends UnitActions.run>(): UnitRunResult;

  // the same idea as above but with different syntax - doesn't work either
  // public abstract act<T = UnitActions.run>(): UnitRunResult;
}

class Pinokio extends WoodenUnit {
  // on this level I want types to be nested from abstract class,
  // because there will be a lot of such Units and copying the same type again and again seems redundant
  public act() {
    return { distance: 20, time: 3 };
  }
}

// some very high-level code that works with Units by their common interface
function test(unit: Unit) {
  switch (unit.actionType) {
    case UnitActions.run: {
      const result = unit.act();
      // do something with results
      // in this place I'm sure what exactly type I use and wanna to get proper type from TS;
      return;
    }
    case UnitActions.fly: {
      // etc..
    }
  }
  return;
}

Maybe my idea with using actionType field to separate logic is wrong and becase of it whole my idea is wrong. In that case how it could be achieved in another way?

CodePudding user response:

Looks to me like you have one act() method that does one of three different and completely unrelated things. So consider having three different methods: run(), fly() and swim() instead of act(). You can put these on different interfaces:

interface RunUnit {
  actionType: UnitActions.run;
  run(): UnitRunResult;
}

interface FlyUnit {
  actionType: UnitActions.fly;
  fly(): UnitFlyResult;
}

interface SwimUnit {
  actionType: UnitActions.swim;
  swim(): UnitSwimResult;
}

Notice the specific types of the actionType fields above; they're not simply actionType: UnitActions but actually allow only one particular value from that enum. This lets you define a discriminated union to distinguish them at runtime:

type ActUnit = RunUnit | FlyUnit | SwimUnit

These actions are now completely decoupled from the Unit class:

abstract class Unit {
}

Implementing units doesn't need a lot of boilerplate now:

abstract class WoodenUnit extends Unit implements RunUnit {
    actionType = UnitActions.run as const;
    abstract run(): UnitRunResult;
}

class Pinokio extends WoodenUnit {
  public run(): UnitRunResult {
    return { distance: 20, time: 3 };
  }
}

Finally, let's call some of these methods. We don't care about the Unit class in this signature at all. All we care about is that it can act, i.e. that it's an ActUnit:

function test(unit: ActUnit) {
  switch (unit.actionType) {
    case UnitActions.run: {
      // We can now call run() because the type has been narrowed to RunUnit!
      const result = unit.run();
      // do something with results
      return;
    }
    case UnitActions.fly: {
      // etc..
    }
  }
  return;
}
  • Related