typescriptoop

Can't access protected property of abstract class from child


I can't access service property of Abstract class from Concrete class using super call, it just works with this. But when I use super, typescript doesn't show any type errors

interface Service {
  do(): void;
}

abstract class Abstract {
  constructor(protected readonly service: Service) {}

  abstract execute(): void;
}

class Concrete extends Abstract {
  constructor(service: Service) {
    super(service);
  }

  execute(): void {
    super.service.do(); // <- if i replace "super" with "this", it works
  }
}

class ServiceImpl implements Service {
  do(): void {
    console.log("i saved something");
  }
}

const concrete = new Concrete(new ServiceImpl());

console.log(concrete.execute()); // <- it will throw error

this code will throw

TypeError: Cannot read properties of undefined (reading 'do')

I tried calling service with super to improve my code readability, so others could easily identify service as a protected member of Abstract class. As Typescript recognised service from super call, by the type checker, i expected it would work, but it doesn't


Solution

  • The super keyword lets you access properties of the prototype of the class. It does not access some hypothetical instance of the superclass (there is no such instance; the field from the superclass is overwritten by the field of the subclass). So if the class member you're referring to is bound to the class instance, then it will be undefined when you access it on super. If you want to access an instance field, you should use this instead of super.

    This doesn't have much to do with protected or abstract classes or properties. You can indeed access methods or accessor properties (those using get) with super, since those do appear on the prototype:

    class Parent {
      constructor() { }
      method() {
        console.log("Parent");
      }
      get accessor() {
        return "Parent";
      }
      prop = "Parent";
    }
    
    class Child extends Parent {
      method() {
        console.log("Child");
      }
      get accessor() {
        return "Child";
      }
      prop = "Child;"
    
      execute(): void {
        this.method(); // "Child"
        super.method(); // "Parent"
        console.log(this.accessor); // "Child"
        console.log(super.accessor); // "Parent"
        console.log(this.prop); // "Child"
        console.log(super.prop); // undefined
      }
    }
    

    So then the real issue you're facing is that your code does not produce a TypeScript error, even though you are accessing an impossible super field. This is apparently a known bug, as reported in microsoft/TypeScript#42214. It looks like TypeScript cannot determine that your service is a field, because it is a parameter property, and thus does not complain. If you rewrite the code to not use parameter properties: you get the expected error:

    abstract class Abstract {
      protected readonly service: Service; // explicitly declare
      constructor(service: Service) {
        this.service = service; // assign
      }
      abstract execute(): void;
    }
    
    class Concrete extends Abstract {
      constructor(service: Service) {
        super(service);
      }
    
      execute(): void {
        super.service.do(); // error!
      }
    }
    

    Playground link to code