GaryO
GaryO

Reputation: 6338

Typescript, field initialiers, and derived classes

I'm creating a parent and child class in Typescript. Each one defines some fields (properties). I'm trying to keep the code clean, using the Partial<T> method for the constructors. But I'm not sure how to pass the parent's properties to the parent in the super() call and initialize the child's props with only the child's unique props in Object.assign() -- if that's even the right paradigm. Here's what I have:

export class Parent {
  name: string = ''
  durationInSec: number = 0.0
  optionalField?: string;
  constructor(init: Partial<Parent>) {
    Object.assign(this, init)
  }
}

export enum Types { A, B, C }

export class ChildA extends Parent {
  kind = Types.A
  childRequired1: string = '' // ??? have to set defaults for all props with this technique
  childOption1?: string
  constructor(init: Partial<ChildA>) {
    super(init) // ??? wrong, passes too many things to super
    Object.assign(this, init) // this would be fine alone, but tsc says need to call super()
  }
}

// similar for ChildB, ChildC etc.
let obj1 = new ChildA({name: 'foo', childRequired1: 'bar'})

Is this even a good paradigm in Typescript? Is there a better way?

Upvotes: 0

Views: 496

Answers (1)

apokryfos
apokryfos

Reputation: 40653

The problem is that init is a Partial<ChildA> so if you don't declare a default for childRequired1 then there's nothing preventing someone from doing new ChildA({}) in fact Parent.durationInSec is actually not optional on parent but you are relying on the default being there.

The second part of the problem is that it appears that using Object.assign(this, ...) in the constructor does not seem to satisfy the requirement of initialising all required parameters in the constructor. There seems to be an open issue about this.

The way I see it you have two options:

  1. Put defaults in all the required types to ensure they always have a value and Partial can still be used

  2. Be much more verbose:

export class Parent {
  name: string;
  durationInSec: number;
  optionalField?: string;
  constructor(init: Parent) {
      this.name = init.name;
      this.durationInSec = init.durationInSec;
      this.optionalField = init.optionalField;
  }
}

export enum Types { A, B, C }

export class ChildA extends Parent {
  kind: Types;
  childRequired1: string;
  childOption1?: string;
  constructor(init: ChildA) {
    super(init as Parent) // Downcasted, it should be ok.
    this.kind = init.kind;
    this.childRequired1 = init.childRequired1;
    this.childOption1 = init.childOption1;
  }
}

let obj1 = new ChildA({
    name: 'foo',
    childRequired1: 'bar',
    durationInSec: 1,
    kind: Types.A
});

Upvotes: 1

Related Questions