Search code examples
typescriptderived-class

Correct TypeScript types for extended constructor parameter in derived class


I have a Base class with a BaseParams constructor parameter, and want to extend a Derived class from Base, that takes an ExtendedParams constructor parameter, and passes it to super(), along with some defaults. The problem is that I can't seem to find the right combination of access modifiers and types for the Derived constructor parameter.

Here's what I have so far:

interface BaseParams {
  first: string;
}

interface ExtendedParams extends BaseParams {
  last?: string;
}

class Base {
  constructor(protected params: BaseParams) {}
  inspect(): void {
    console.log(this.params);
  }
}

class Derived extends Base {
  constructor(??? params ???) {
    super({ first: 'John', last: 'default', ...params });  // override default params with specified ones
    this.params.last;  // #1 - this should not generate any compile errors
    this.params.whatever = 'Wrong';  // #2 - TS should flag this as not existing on type ExtendedParams
  }
}

const d = new Derived({ first: 'Mike' });
d.inspect();  // #3 - should output { first: 'Mike', last: 'default', whatever: 'Wrong' }

I tried declaring the Derived params as protected. That solves #1 and #2.

class Derived extends Base {
  constructor(protected params: ExtendedParams) {
    super({ first: 'John', last: 'default', ...params });
    this.params.last;  // #1 - ok, no error
    this.params.whatever = 'Wrong';  // #2 - ok, flagged: `whatever` doesn't exist on type ExtendedParams
  }
}

The problem is that the last: 'default' assignment doesn't take place because protected generates a this.params = params assignment that overwrites the params value passed to super(), so d.inspect() outputs only { first: 'Mike', whatever: 'Wrong' }.

I tried omitting the access modifier for params. This leads to the correct output { first: 'Mike', last: 'default', whatever: 'Wrong' }, but also to this.params.last being flagged as an error, does not exist on type 'BaseParams'.

class Derived extends Base {
  constructor(params: ExtendedParams) {
    super({ first: 'John', last: 'default', ...params });
    this.params.last;  // #1 - wrong ly flagged 
    this.params.whatever = 'Wrong';  // #2 - ok, flagged
  }
}

Is there some TypeScript magic to inform the compiler that in the Derived class, this.params is of ExtendedParams type, and the defaults get passed to the super() call and this.params is not overwritten afterwards?


Solution

  • But from Derived you access Base.params which is clearly typed as BaseParams.

    If you want the params property to change type in derived classes, you'd need a generic base type:

    // <P = BaseParams> allows to instantiate Base with BaseParams as default type.
    class Base<P = BaseParams> {
        constructor(protected params: P) {}
        /* ... */
    }
    
    class Derived extends Base<ExtendedParams> {
        constructor(params: ExtendedParams) {
            super({ first: 'John', last: 'default', ...params });  // override default params with specified ones
            this.params.last;                // OK
            this.params.whatever = 'Wrong';  // Wrong
        }
    }