在JavaScript中深度合并后保留自动完成的键/值。

huangapple go评论80阅读模式
英文:

Preserve key/values for autocomplete after deep merge in JavaScript

问题

我正在编写一个相当简约的配置系统。思路是有一个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),
    ),
};

可能不是最干净的方法,但完美运行!

英文:

I'm writing a rather minimalistic config system. The idea is having a config.template.js and a config.custom.js. Now, all set values from custom should override those in template. Values missing from custom will be read from template.

The logic for that looks something like this:

const isObject = item =&gt; item &amp;&amp; typeof item === &quot;object&quot; &amp;&amp; !Array.isArray(item);

const deepMerge = function(target, source){
    if (isObject(target) &amp;&amp; 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(&quot;./config.custom.js&quot;)).default;
const configBase = (await import(&quot;./config.template.js&quot;)).default;

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

Now my problem:

VSCode doesn't know anything about how the resulting config actually looks like. So no autocomplete or types for keys.

VSCode would be able to provide autocomplete if I'd just do:

export const config = {
    ...configBase,
    ...configCustom,
};

This however results in a shallow copy for nested keys, effectively overriding entire objects/arrays with, well, nothing.

Since I already make heavy use of JSDoc, I thought I could annotate the deepMerge function like

/**
 * @param {object} target
 * @param {object} source
 * @return {import(&quot;./config.template.js&quot;).default}
 */

but that, of course, was wishful thinking and does not work.

So my question:

How can I provide autocomplete / types for this config system without relying on shallow copies?


<sub>
I know that there are a ton of config systems and this is kinda re-inventing the wheel. I still want to understand and learn. <br>
And yes, TypeScript would make this easier.
</sub>


UPDATE:

@creepsore's answer worked perfectly well. However I had to change a couple of annotations because I got some overloading errors <br>
(raised by VSCode with &quot;js/ts.implicitProjectConfig.checkJs&quot;: true):

/**
 * @template {object} T
 * @template {object} T2
 * @param {T} target
 * @param {T2 &amp; Partial&lt;T&gt;} source
 * @returns {T &amp; T2}
 */
const deepMerge = function(target, source){
    if (isObject(target) &amp;&amp; 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 &amp; T2} */ (target);
};

// ...

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

Probably not the cleanest approach, but works perfectly well!

答案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),
};

这些是我用于测试的示例配置:

// config.template.js
export default {
    a: 420,
    b: 1337,
    c: 360
};

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

在JavaScript中深度合并后保留自动完成的键/值。


<details>
<summary>英文:</summary>

It works when you define the two parameters of deepMerge as generics and use these as it&#39;s return type as such:

    const isObject = item =&gt; item &amp;&amp; typeof item === &quot;object&quot; &amp;&amp; !Array.isArray(item);
    
    /**
     * @template T
     * @template T2
     * @param {T} target 
     * @param {T2} source 
     * @returns {T&amp;T2}
     */
    const deepMerge = function(target, source){
        if (isObject(target) &amp;&amp; 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(&quot;./config.custom.js&quot;)).default;
    const configBase = (await import(&quot;./config.template.js&quot;)).default;
    
    export const config = {
        ...deepMerge(configBase, configCustom),
    };

[![Example][1]][1]

These are my example configs i used for testing:

    // config.template.js
    export default {
        a: 420,
        b: 1337,
        c: 360
    };

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

  [1]: https://i.stack.imgur.com/W5Egn.png

</details>



huangapple
  • 本文由 发表于 2023年6月22日 16:27:02
  • 转载请务必保留本文链接:https://go.coder-hub.com/76529938.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定