Method overloading is one of the more common patterns in OOP but typescript does not have proper method overloading, as the best we could do is create multiple different signatures of the same method name, but only one actual implementation.

I came to need this in XJS 3, as one of our objectives was to cut down the bundle size when compared to XJS 2. In XJS 2, we had different classes for each context, which calls the same underlying core function. The developer experience of that approach was great, as we could define the right method names and parameters based on what it actually does.

const scene = await xjs.Scene.getActiveScene();
const sceneName = await scene.getName();

// IDE/Editors with good typescript support would hint that
// the expected parameter is a string:
scene.setName('ola');

The downside of this approach was that each context had to have their own class, which ballooned the total package size, even though the underlying implementation were similar. A prime example for this is all the item-related classes.

The Problem

The knee-jerk reaction to the size of XJS 2 was to create a minimalistic library that does not have individual classes for each item types, to avoid duplication. One way we did this was to have a single App class and a single Item class, with a generic setProperty and getProperty that would accept a JSON object that would define the underlying core property, and its necessary transformations. A second parameter would be used for additional information and/or for setting value.

The problem that arose here, although it works and the bundled size is significantly smaller than XJS 2, is that the developer experience was way worse than before.

Here's an example on my struggles when I was dog fooding it on some newer plugins:

import Xjs from 'xjs-framework/core/xjs';
import App from 'xjs-framework/core/app';
import AppProps from 'xjs-framework/props/app-props';

const xjs = new Xjs();
const app = new App(xjs);

// Getting the scene index
app.getProperty(AppProps.sceneIndex, { view: 1 });

// Getting the scene name
app.getProperty(AppProps.sceneName, { scene: 0 });

// Setting the scene name
app.setProperty(AppProps.sceneName, { scene: 0, value: 'ola' });

// Setting the scene configuration
app.setProperty(AppProps.scene, '<configuration>...some xml...</configuration>');

The main struggle I ran into was that I had to frequently refer to the docs or source code to figure out what the structure is for the second parameter, which is different depending on the property passed on the first parameter.

The Solution

Considering that reverting back to a separate method for each property isn't ideal and is against the whole XJS 3's objectives, the best we could do is provide a better documentation and API reference. Or so I thought.

A better experience would be if typescript could hint as to what would the second parameter's structure be based on the object passed on its first parameter. This could be achieved with method overloading, defining a different signature depending on the property's structure.

In Typescript, the closest we could get with method overloading is defining different method signatures, but the actual implementation would only be one, as the language it transpiles to, javascript, does not have method overloading (duh, no types).

The result is brilliant, the bundle size is still the same, as the additional method signatures are only added to the .d.ts files. The transpiled JS files remained the same as before we added this.

  setProperty(prop: IAudioProperty, param: string): Promise<string>;
  setProperty(prop: ISceneProperty, param: string): Promise<string>;
  setProperty(
    prop: ISceneSceneProperty,
    param: ISceneSceneParam
  ): Promise<string>;
  setProperty(
    prop: ISceneViewProperty,
    param: ISceneViewParam
  ): Promise<string>;
  setProperty(
    prop: ISceneSceneWidthHeightProperty,
    param: ISceneSceneWidthHeightParam
  ): Promise<string>;

  /**
   * Set application property
   *
   * @param prop Application Property object
   * @param param Params that would be passed to the underlying core function
   */
  setProperty(prop: IPropertyType, param: PropertyParam): Promise<string> {
    // The actual implementation here...
  }

The resulting developer experience was better than what we had before, as VSCode or WebStorm (or any editor with good typescript support for that matter) would hint as to what the second parameter would look like, and error out if you did not pass the expected structure:

The difference now is that you won't have to rely on looking at the error messages at runtime, which is what used to happen before, and instead, get the error message on your editor right away.

It would've been super great if VSCode allowed you to click the type in the hint, so that we could look at the expected structure of the second parameter.

Here's the pull request: https://github.com/dcefram/xjs/pull/37