如何确保Closure Compiler高级模式下两个typedef类型的属性重命名一致以支持对象合并?
你的问题核心在于Closure Compiler在高级优化模式下,会把无关联的typedef类型的属性当作独立标识进行混淆重命名——两个typedef里的attribute1会被编译成不同的短名称,导致Object.assign无法正确匹配合并。要解决这个问题,关键是让编译器知道Params和Options的属性是同一套结构,只是可选性不同,下面给你几个靠谱的解决方案:
方案一:用@record标记基础类型,关联Options类型
这是最推荐的做法,通过@record定义一个标准的参数结构,再让Options成为这个结构的可选版本,编译器会自动保持属性重命名的一致性:
/** * @record */ function Params() {} /** @type {string} */ Params.prototype.attribute1; /** @type {string} */ Params.prototype.attribute2; /** @type {string} */ Params.prototype.attribute3; /** @typedef {Partial<Params>} 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", }; } } /* 创建自定义实例,仅覆盖一个属性 */ const model = new Model({ attribute2: "custom value", });
这里@record告诉编译器Params是一个具有固定属性结构的类型,Partial<Params>则直接复用这个结构,将所有属性设为可选。编译时,两个类型的对应属性会被赋予相同的混淆名称,Object.assign就能正常工作了。
方案二:让Options直接引用Params的属性结构
如果不想用@record,可以在Options的typedef里明确关联Params的属性,避免重复定义导致的结构割裂:
/** * @typedef {{ * attribute1: string, * attribute2: string, * attribute3: string * }} Params */ /** * @typedef {Partial<Params>} 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", }; } } const model = new Model({ attribute2: "custom value", });
这种方式同样让编译器识别到Options是Params的子集,属性重命名时会保持一致。如果你的编译器版本不支持Partial,可以手动写Options的typedef,但要严格复用Params的属性名:
/** * @typedef {{ * attribute1: (string|undefined), * attribute2: (string|undefined), * attribute3: (string|undefined) * }} Options */
注意必须保证属性名和Params完全一致,不能有拼写错误,否则还是会出现重命名不匹配的问题。
方案三:用@preserve强制保留属性名(不推荐)
如果临时不想修改类型定义,可以给每个属性加上@preserve标签,让编译器不混淆这些属性名:
/** * @typedef {{ * /** @preserve */ * attribute1: string, * /** @preserve */ * attribute2: string, * /** @preserve */ * attribute3: string * }} Params */ /** * @typedef {{ * /** @preserve */ * attribute1: (string|undefined), * /** @preserve */ * attribute2: (string|undefined), * /** @preserve */ * attribute3: (string|undefined) * }} Options */
这种方法虽然能解决问题,但会失去高级模式下代码压缩的优势,只适合临时调试或特殊场景。
关键注意事项
- 确保使用的Closure Compiler版本支持
Partial类型(v20190909及以上版本都支持); - 永远不要在两个typedef中重复定义属性却不关联它们的结构,这是导致重命名不匹配的根本原因;
- 如果你用了
@record,返回的默认值对象要严格符合Params的结构,避免编译器产生类型歧义。
备注:内容来源于stack exchange,提问作者Romain

