在JavaScript中进行深度合并后,保留自动完成的关键字/值对。

3
我正在编写一个相当简约的配置系统。思路是有一个config.template.js和一个config.custom.js。现在,所有来自custom的设置值应该覆盖template中的值。从custom中缺失的值将从template中读取。
这个逻辑大致如下:
const isObject = item => item && typeof item === "object" && !Array.isArray(item);

const deepMerge = function(target, source){
    if (isObject(target) && isObject(source)){
        for (const key in source){
            if (isObject(source[key])){
                if (!target[key]) target[key] = {};
                deepMerge(target[key], source[key]);
            }
            else target[key] = source[key];
        }
    }
    return target;
};

// ...

const configCustom = (await import("./config.custom.js")).default;
const configBase = (await import("./config.template.js")).default;

export const config = {
    ...deepMerge(configBase, configCustom),
};

现在我的问题是:
VSCode对生成的配置文件的实际外观一无所知。因此,无法为键提供自动完成或类型提示。
如果我只是这样做,VSCode将能够提供自动完成:
export const config = {
    ...configBase,
    ...configCustom,
};

这导致嵌套键的浅拷贝,实际上用空值覆盖了整个对象/数组。
由于我已经大量使用JSDoc,所以我想我可以像这样注释deepMerge函数。
/**
 * @param {object} target
 * @param {object} source
 * @return {import("./config.template.js").default}
 */

但是,那当然只是一厢情愿的想法,并不起作用。
所以我的问题是:
我如何在不依赖浅拷贝的情况下为这个配置系统提供自动完成/类型支持?
我知道有很多配置系统,这有点是在重复造轮子。但我仍然想要理解和学习。
而且,使用TypeScript会让这更容易。

更新:

@creepsore答案非常完美地解决了问题。不过,由于我遇到了一些重载错误(由VSCode的"js/ts.implicitProjectConfig.checkJs": true引发),我不得不更改了一些注释。

/**
 * @template {object} T
 * @template {object} T2
 * @param {T} target
 * @param {T2 & Partial<T>} source
 * @returns {T & T2}
 */
const deepMerge = function(target, source){
    if (isObject(target) && isObject(source)){
        for (const key in source){
            if (isObject(source[key])){
                if (!target[key]) target[key] = {};
                deepMerge(target[key], source[key]);
            }
            else target[key] = source[key];
        }
    }
    return /** @type {T & T2} */ (target);
};

// ...

export const config = {
    ...deepMerge(
        configBase,
        /** @type {Partial<typeof configBase>} */ (configCustom),
    ),
};

可能不是最干净的方法,但完全可以正常运行!

1
你不能写一个浅层次的配置吗?所以,你可以将{ opt1: { opt1_1: 'val', opt_1_2: 'val'}, opt2: 'val'}改成{'opt1.opt1_1': 'val', 'opt1.opt1_2': 'val', 'opt2': 'val'} - yunzen
1
你能不能写一个浅层配置呢?所以,不要写成{ opt1: { opt1_1: 'val', opt_1_2: 'val'}, opt2: 'val'},而是写成{'opt1.opt1_1': 'val', 'opt1.opt1_2': 'val', 'opt2': 'val'} - yunzen
1
你能不能写一个浅层配置呢?所以,不要写成{ opt1: { opt1_1: 'val', opt_1_2: 'val'}, opt2: 'val'},而是写成{'opt1.opt1_1': 'val', 'opt1.opt1_2': 'val', 'opt2': 'val'} - undefined
@yunzen 很遗憾,不行。虽然配置系统本身很基本,但是个别配置是嵌套的。 - NullDev
我猜测嵌套是你使用浅拷贝时遇到的问题。这就是为什么我建议你使用键的名称作为嵌套的原因。 - yunzen
显示剩余4条评论
1个回答

2
当你将deepMerge的两个参数定义为泛型,并将它们用作返回类型时,它就能正常工作。
const isObject = item => item && typeof item === "object" && !Array.isArray(item);

/**
 * @template T
 * @template T2
 * @param {T} target 
 * @param {T2} source 
 * @returns {T&T2}
 */
const deepMerge = function(target, source){
    if (isObject(target) && isObject(source)){
        for (const key in source){
            if (isObject(source[key])){
                if (!target[key]) target[key] = {};
                deepMerge(target[key], source[key]);
            }
            else target[key] = source[key];
        }
    }
    return target;
};

const configCustom = (await import("./config.custom.js")).default;
const configBase = (await import("./config.template.js")).default;

export const config = {
    ...deepMerge(configBase, configCustom),
};

Example

这些是我用于测试的示例配置文件:
// config.template.js
export default {
    a: 420,
    b: 1337,
    c: 360
};

// config.custom.js
export default {
    b: 420
};

这个完美地运作了,谢谢! - NullDev
这个完美地运作,谢谢! - NullDev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接