jsdocgoogle-closure-compiler

Is it possible to tell closure compiler that 2 typedef intersect?


I have a class for a model that contains a configuration, a plain object, with default values, and I want to create instances of this class by overriding only some of the configuration keys by passing another plain object, the options. I also want the code to work properly after being compiled in advanced mode and the keys of the configuration and options renamed.

/**
 * @typedef {{
 *     attribute1: string,
 *     attribute2: string,
 *     attribute3: string
 * }}
 */
let Params;

/**
 * @typedef {{
 *     attribute1: (string|undefined),
 *     attribute2: (string|undefined),
 *     attribute3: (string|undefined)
 * }}
 */
let Options;

class Model
{
    /**
     * @param {!Options=} options
     */
    constructor(options = {})
    {
        /** @private {!Params} */
        this.params = /** @type {!Params} */(Object.assign(this.getDefaults(), options));
    }

    /**
     * @return {!Params}
     */
    getDefaults()
    {
        return {
            attribute1: "default value 1",
            attribute2: "default value 2",
            attribute3: "default value 3",
        };
    }
}

/* Create a custom model by overriding only one attribute */
const model = new Model({
    attribute2: "custom value",
});

My problem is I don't know how to tell the compiler that types "Params" and "Options" shoud keep the same keys.
In advanced mode the properties will be renamed to something different. For example Params.attribute1 will become Params.a and Options.attribute1 will become Options.b so I cannot merge them anymore.
Is there a way to make sure that the renamed properties still match ?


Solution

  • OK I found a working solution. Seems like @record is a special type that automatically intersect any other type it can. With the solution below type checking works and properties are renamed without breaking anything.

    /** @record */
    const Params = function() {};
    /** @type {string} */Params.prototype.attribute1;
    /** @type {string} */Params.prototype.attribute2;
    /** @type {string} */Params.prototype.attribute3;
    
    /** @record */
    const Options = function() {};
    /** @type {(string|undefined)} */Options.prototype.attribute1;
    /** @type {(string|undefined)} */Options.prototype.attribute2;
    /** @type {(string|undefined)} */Options.prototype.attribute3;
    
    class Model
    {
        /**
         * @param {!Options=} options
         */
        constructor(options = {})
        {
            /** @private {!Params} */
            this.params = /** @type {!Params} */(Object.assign(this.getDefaults(), options));
        }
    
        /**
         * @return {!Params}
         */
        getDefaults()
        {
            return {
                attribute1: "default value 1",
                attribute2: "default value 2",
                attribute3: "default value 3",
            };
        }
    }
    
    /* Create a custom model by overriding only one attribute */
    const model = new Model({
        attribute2: "custom value",
    });