英文:
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 => 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),
};
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("./config.template.js").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 "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),
),
};
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
};
<details>
<summary>英文:</summary>
It works when you define the two parameters of deepMerge as generics and use these as it's return type as such:
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][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>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论