Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript Inheritance: Expanding base class object property

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 is 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).

like image 906
Clorofilla Avatar asked Dec 06 '21 18:12

Clorofilla


People also ask

Can we create object of base class in inheritance?

No it will not create an objects of the base class, the inherited class's objects can have accessibility to the base class properties(according to the protection level). so that the particular members(available to the inherited class) are only initialized, no object of base class is created.

What is the correct way of inheriting in TypeScript?

The TypeScript uses class inheritance through the extends keyword. TypeScript supports only single inheritance and multilevel inheritance.

How do I extend a class in TypeScript?

Just like object-oriented languages such as Java and C#, TypeScript classes can be extended to create new classes with inheritance, using the keyword extends . In the above example, the Employee class extends the Person class using extends keyword.

Is there inheritance in TypeScript?

TypeScript supports single inheritance and multilevel inheritance. We can not implement hybrid and multiple inheritances using TypeScript. The inheritance uses class-based inheritance and it can be implemented using extends keywords in typescript.


3 Answers

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 at Stage 4 of the TC39 process and will be part of ES2022 (currently in draft at 2021-12-07). 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

like image 62
jcalz Avatar answered Oct 18 '22 05:10

jcalz


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.

like image 9
Qwertiy Avatar answered Oct 18 '22 05:10

Qwertiy


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 define a new options field in the Wizard class (which would have also hidden Human.options if you had.

Please see @jcalz's answer for what I think is the best solution for this case.

like image 1
Inigo Avatar answered Oct 18 '22 05:10

Inigo