我在这里看到的问题与递归类型几乎没有关系,而与尝试访问 union-typed 对象上的属性有很大关系。
假设您有一个以下联合类型的变量 something。
declare const something: HTMLElement | { prop: HTMLElement };
所有编译器都知道something 是或者一个HTMLElement或者一个具有prop 属性类型为HTMLElement 的对象。如果您只是将其视为HTMLElement,编译器(正确地)会警告您它不知道可以将其视为一个:
something.innerHTML.toUpperCase(); // error!
// -----> ~~~~~~~~~
// Property 'innerHTML' does not exist on type '{ prop: HTMLElement; }'
将其解读为:“我无法确定这是 HTMLElement 还是 {prop: HTMLElement},因此我无法确定是否存在 innerHTML 属性。”
如果你想访问innerHTML,你需要让编译器相信这样做是安全的。这是一种可能的方法:
if ("style" in something) {
something.innerHTML.toUpperCase(); // okay
}
通过检查"style" 是否是something 的键,您已经让编译器相信something 是HTMLElement 而不是{prop: HTMLElement},因为(它假设)该属性的存在可以用于区分这两种类型。
这正是您遇到的问题。假设您有一个接受iROGeneric<HTMLElement> 的函数。你不能仅仅假设它的属性是HTMLElements 而不让编译器相信这个事实:
function doSomething(foo: iROGeneric<HTMLElement>) {
Object.keys(foo).forEach(k => {
const prop = foo[k];
console.log(prop.tagName.toUpperCase()); // error!
}
}
相反,您需要想出一个测试来区分 HTMLElement 和 iROGeneric<HTMLElement>:
function doSomething(foo: iROGeneric<HTMLElement>) {
Object.keys(foo).forEach(k => {
const prop = foo[k];
if (prop instanceof HTMLElement) {
console.log(prop.tagName.toUpperCase()); // okay
} else {
doSomething(prop); // okay
}
})
}
备份,您的问题也可能是您希望编译器记住Html 的特定结构。但是您已将其注释为iROGeneric<HTMLElement> 类型。因此,编译器会尽职尽责地忘记您初始化变量所使用的特定结构,并将其类型加宽为iROGeneric<HTMLElement>。如果您不希望编译器这样做,那么不要注释它:
const Html2 = {
body: document.querySelector<HTMLElement>("body")!,
head: document.querySelector<HTMLElement>("head")!,
article: {
main: document.querySelector<HTMLElement>("main > article")!,
aside: document.querySelector<HTMLElement>("aside > article")!
}
oops: "sorry" // no error here
}
Html2.main.insertAdjacentHTML("beforebegin", ""); // oops
Html2.article.main.insertAdjacentHTML("beforebegin", ""); // okay
如果您担心编译器无法验证 Html2 是否可分配给 iROGeneric<HTMLElement>,您应该问问自己为什么这很重要。如果你将它传递给某个假定它是iROGeneric<HTMLElement> 并且它不是的函数,你会得到一个错误那里。但是,如果您想尽早发现它,您可以创建一个身份辅助函数来检查但不扩大类型:
const asIROGenericHtmlElement = <T extends iROGeneric<HTMLElement>>(t: T) => t;
const Html3 = asIROGenericHtmlElement({
body: document.querySelector<HTMLElement>("body")!,
head: document.querySelector<HTMLElement>("head")!,
article: {
main: document.querySelector<HTMLElement>("main > article")!,
aside: document.querySelector<HTMLElement>("aside > article")!
},
oops: "sorry" // error!
// ~~~~ <-- string is not HTMLElement | iROGeneric<HTMLElement>
})
嘿,一个错误:让我们修复它:
const Html3 = asIROGenericHtmlElement({
body: document.querySelector<HTMLElement>("body")!,
head: document.querySelector<HTMLElement>("head")!,
article: {
main: document.querySelector<HTMLElement>("main > article")!,
aside: document.querySelector<HTMLElement>("aside > article")!
},
// oops: "sorry" // comment this out
})
Html3.article.main.insertAdjacentHTML("beforebegin", ""); // okay
Html3 解决方案通常是两全其美的解决方案:编译器会记住对象的确切属性和类型而不会扩大,但如果它不符合扩大的类型,仍然会抱怨。
Playground link to code