Home > Net >  Typescript Inheritance: Expanding base class object property
Typescript Inheritance: Expanding base class object property

Time:12-07

I am struggling.

When extending a class I can easily add some new properties to it.

But what if, when I extend a base class, I want to add new properties to an object (a property which is a simple object) of the base class?

Here an example with some code.

base class

type HumanOptions = {
  alive: boolean
  age: number
}

class Human {
  id: string
  options: HumanOptions

  constructor() {
    this.id = '_'   Math.random()

    this.options = {
      alive: true,
      age: 25,
    }

  }
}

derived class

type WizardOptions = {
  hat: string
} & HumanOptions

class Wizard extends Human{
 
  manaLevel: number
  options: WizardOptions // ! Property 'options' has no initializer and is not definitely assigned in the constructor.

  constructor() {
    super()
    this.manaLevel = 100
    this.options.hat = "pointy" // ! Property 'options' is used before being assigned.
  }
}

Now, as you can see from the in-line comments, this will anger TypeScript. But this works in JavaScript. So what is the correct ts way to achive this? If there is not, is the problem in my code-pattern itself? What pattern would be suited for the problem?

Maybe you are wondering why I want those properties in a options object. It can be very useful! For example: only the properties in the options are be edited through some UI. So, some other code could dynamically look for the options in the class and expose/update them on the UI, meanwhile properties like id and manaLevel would be left alone.

Note

I know I could do this for the wizard:

class Wizard extends Human{
 
  manaLevel: number

  options: {
    hat: string
    alive: boolean
    age: number
  }

  constructor() {
    super()
    this.manaLevel = 100
    this.options = {
      alive: true,
      age: 25,
      hat: "pointy"
    }
  }
}

And it works. but then the code is not very DRY and I have to manually manage the options inheritance (which in a complex application is not ideal).

CodePudding user response:

If you're just going to reuse a property from a superclass but treat it as a narrower type, you should probably use the declare property modifier in the subclass instead of re-declaring the field:

class Wizard extends Human {
  manaLevel: number
  declare options: WizardOptions; // <-- declare
  constructor() {
    super()
    this.manaLevel = 100
    this.options.hat = "pointy" // <-- no problem now
  }
}

By using declare here you suppress any JavaScript output for that line. All it does is tell the TypeScript compiler that in Wizard, the options property will be treated as a WizardOptions instead of just a HumanOptions.


Public class fields are currently at Stage 3 of the TC39 process and therefore very likely to be part of JavaScript in their current form, or close to it. When that happens (or maybe now if you have a new enough JS engine), the code you wrote will end up producing JavaScript that looks like

class Wizard extends Human {
    manaLevel;
    options; // <-- this will be here in JavaScript
    constructor() {
        super();
        this.manaLevel = 100;
        this.options.hat = "pointy";
    }
}

which is just your TypeScript without type annotations. But that options declaration in JavaScript will initialize options in the subclass to undefined. And thus it really will be true that this.options is used before being assigned:

console.log(new Wizard()); // runtime error!
// this.options is undefined

So the error you're getting from TypeScript is a good one.


Originally TypeScript implemented class fields so that just declaring a field wouldn't have an effect if you didn't initialize it explicitly. That was how the TypeScript team thought class fields would eventually be implemented in JavaScript. But they were wrong about that. If your compiler options don't enable --useDefineForClassFields (and notice that if you make --target new enough it will be automatically included; see microsoft/TypeScript#45653) then it will output JS code where no runtime error occurs for your example.

So you could argue that the compiler should not give you an error if you are not using --useDefineForClassFields. Before --useDefineForClassFields existed, such an argument was actually made at microsoft/TypeScript#20911 and microsoft/TypeScript#21175.

But I don't think the TS team is too receptive to this anymore. As time goes on, declared class fields in JavaScript being initialized to undefined will be the norm and not the exception, and supporting the older nonconforming version of class fields isn't anyone's priority. See microsoft/TypeScript#35831 where the suggestion is basically "use declare". Which is what I'm suggesting here too.

Playground link to code

CodePudding user response:

This is partial answer which I'll update as you provide more info. e.g. answers to questions under your question above.

Your second error is results from the options field you declare in Wizard hiding the options field you declared in Human. This is how most (all) class-based type systems work, not just TS. Thus Wizard.options is undefined at the point this.options.hat = "pointy" executes. You can see this by commenting out options: WizardOptions

This worked in Javascript because you relied on prototypical inheritance, and didn't declare a new options field in the Wizard class (which would have also hidden Human.options if you had.

CodePudding user response:

You can do it in a following way: playground

type HumanOptions = {
  alive: boolean
  age: number
}

class Human<Options extends HumanOptions> {
  id: string
  options: Options

  constructor() {
    this.id = '_'   Math.random()

    this.options = {
      alive: true,
      age: 25,
    } as Options
  }
}

type WizardOptions = {
  hat: string
} & HumanOptions

class Wizard extends Human<WizardOptions> {
  manaLevel: number

  constructor() {
    super()
    this.manaLevel = 100
    this.options.hat = "pointy"
  }
}

Note, that you are fully responsible on setting all required props of options.

  • Related