Search code examples
javascripttypescriptobjecttypesparameters

Modifying a parent class's constructor parameters and passing them to `super()`


I have a base class, Sprite, and a subclass Player. Sprite's constructor() takes a few parameters, one of which is the path to an image. Player should take all parameters except the image path, and should take a new parameter which it will use to determine the image path itself.

The question Is there a way to inherit parameters from a parent class's constructor? led me to ConstructorParameters:

// Sprite.ts

import SomeVendoredRenderLibrary from "./vendored/SomeVendoredRenderLibrary.js";

export default abstract class Sprite {
  private _image: HTMLImageElement;

  public constructor(
    protected canvasWidth: number,
    protected canvasHeight: number,
    imagePath: string
  ) {
    this._image = SomeVendoredRenderLibrary.loadImage(imagePath);
  }
}

// Player.ts

import Sprite from "./Sprite.js";

export default class Player extends Sprite {
  constructor(
    type: "modelA" | "modelB",
    ...args: ConstructorParameters<typeof Sprite>
  ) {
    // this doesn't work! 
    args.imagePath = `assets/player_${type}.png`;
    super(...args);
  }
}

Of course, the above code fails because args is an array and hence can't be assigned by name. Furthermore, ConstructorParameters<typeof Sprite> requires imagePath to be passed explicitly, although it would be redundant.

It would of course be possible to explicitly add each required parameter of Sprite to Player and its other subclasses, as parameters to constructor() and arguments to super(), but this seems like a hacky workaround, as it would require manually updating all subclasses should any changes be made to Sprite.

What's the canonical way to inherit/passthrough constructor() parameters from a parent class, while potentially modifying some?


Solution

  • From the above comments ...

    @PeterSeliger yes, I know it's an array, but if I were to add a parameter to Sprite that changes the position of imagePath, subclasses would be broken. i've edited my question to clarify that. i was hoping there would be a better way? – theo

    It doesn't matter whether it's ...args, args or passing parameters directly to the function; as long as parameters are reflected as arguments array the problem of breaking subclasses when changing the arguments' signature does remain. The only way of preventing the latter is to make use of a single object based parameter which features all of the former parameters as its properties. – Peter Seliger

    One could do it by either extending an already existing type ...

    import SomeVendoredRenderLibrary from "./vendored/SomeVendoredRenderLibrary.js";
    
    type PlayerOptions = {
      canvasWidth: number,
      canvasHeight: number,
    };    
    type SpriteOptions = PlayerOptions & {
      imagePath: string,
    };
    
    class Sprite {
    
      // ... modifier declaration
    
      constructor({ canvasWidth, canvasHeight, imagePath }:SpriteOptions) {
    
        // ... assignments
      }
    }
    
    class Player extends Sprite {
      constructor(
        type: "modelA" | "modelB",
        options: PlayerOptions,
      ) {
        super({ ...options, imagePath: `assets/player_${ type }.png`});
      }
    }
    

    ... or by defining a single (meta) type for any constructor's single parameter and explicitly omitting properties as suggested by Shalom Peles ...

    import SomeVendoredRenderLibrary from "./vendored/SomeVendoredRenderLibrary.js";
    
    type SpriteOptions = {
      canvasWidth: number,
      canvasHeight: number,
      imagePath: string,
    };
    
    class Sprite {
    
      // ... modifier declaration
    
      constructor({ canvasWidth, canvasHeight, imagePath }:SpriteOptions) {
    
        // ... assignments
      }
    }
    
    class Player extends Sprite {
      constructor(
        type: "modelA" | "modelB",
        options: Omit<SpriteOptions, 'imagePath'>
      ) {
        super({ ...options, imagePath: `assets/player_${ type }.png`});
      }
    }