【问题标题】:How to declare and call nested interfaces in TypeScript?如何在 TypeScript 中声明和调用嵌套接口?
【发布时间】:2021-01-20 03:37:20
【问题描述】:

我正在尝试将 JavaScript 转换为 TypeScript。我一直在寻找有关“递归”或“嵌套”接口等主题的答案,但没有找到有用或可理解的答案。

以下结构坚决反对我之前的尝试:

interface iROGeneric<T> {
    readonly [key: string]: T | iROGeneric<T>;
};

const Html: iROGeneric<HTMLElement> = {
    body: document.querySelector<HTMLElement>("body"),
    head: document.querySelector<HTMLElement>("head"),
    article: {
        main: document.querySelector<HTMLElement>("main > article"),
        aside: document.querySelector<HTMLElement>("aside > article")
    }
}

当我尝试访问单个对象元素时,例如

Html.main.insertAdjacentHTML(...);

我在 compile.time 期间收到以下错误:

“HTMLElement | 类型”上不存在“主”属性iROGeneric'。
“HTMLElement”类型上不存在属性“main”。

另外,还有如下错误提示:

此表达式不可调用。
并非所有类型为 'HTMLElement | iRO通用 | { (选择器:K): HTMLElementTagNameMap[K]; (选择器:K):SVGElementTagNameMap[K]; 类型“HTMLElement”没有调用签名。

我也明白有些人会想“你为什么要这样做?更好的选择是......”。我想更好地了解接口的使用,以避免将来出现错误。

【问题讨论】:

    标签: typescript


    【解决方案1】:

    我在这里看到的问题与递归类型几乎没有关系,而与尝试访问 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 的键,您已经让编译器相信somethingHTMLElement 而不是{prop: HTMLElement},因为(它假设)该属性的存在可以用于区分这两种类型。


    这正是您遇到的问题。假设您有一个接受iROGeneric&lt;HTMLElement&gt; 的函数。你不能仅仅假设它的属性是HTMLElements 而不让编译器相信这个事实:

    function doSomething(foo: iROGeneric<HTMLElement>) {
        Object.keys(foo).forEach(k => {
            const prop = foo[k];
            console.log(prop.tagName.toUpperCase()); // error!
        }
    }
    

    相反,您需要想出一个测试来区分 HTMLElementiROGeneric&lt;HTMLElement&gt;

    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&lt;HTMLElement&gt; 类型。因此,编译器会尽职尽责地忘记您初始化变量所使用的特定结构,并将其类型加宽iROGeneric&lt;HTMLElement&gt;。如果您不希望编译器这样做,那么不要注释它

    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&lt;HTMLElement&gt;,您应该问问自己为什么这很重要。如果你将它传递给某个假定它是iROGeneric&lt;HTMLElement&gt; 并且它不是的函数,你会得到一个错误那里。但是,如果您想尽早发现它,您可以创建一个身份辅助函数来检查但不扩大类型:

    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

    【讨论】:

      【解决方案2】:
      const Html: iROGeneric<HTMLElement> = {
          body: document.querySelector<HTMLElement>("body"),
          head: document.querySelector<HTMLElement>("head"),
          article: {
              main: document.querySelector<HTMLElement>("main > article"),
              aside: document.querySelector<HTMLElement>("aside > article")
          }
      }
      

      这看起来不错,除了 article 类型,你应该更喜欢这个:

      const Html: iROGeneric<HTMLElement> = {
          body: document.querySelector<HTMLElement>("body"),
          head: document.querySelector<HTMLElement>("head"),
          article: IArticle<HTMLElement>
      }
      
      interface  IArticle<HTMLElement> {
           main: document.querySelector<HTMLElement>("main > article"),
           aside: document.querySelector<HTMLElement>("aside > article")
      }
      

      您应该始终为嵌套对象定义单独的接口,这是唯一的方法。希望您会发现这对您有所帮助,并为将来相应地定义您的接口。

      【讨论】:

      • 感谢您的快速回复。然而,对于复杂的对象结构,必须声明许多接口。可以使用通用接口吗?喜欢文章:{…} as iROGeneric
      • 这是我们不能忽视的过程,你要定义这么多接口。但这是一件好事,但如果您在 100 多个接口中使用该结构,并且如果您想再添加一个属性,那将是一件令人头疼的事情。这就是为什么不允许那样做的原因。而后一部分,我从未尝试过,我建议您创建单独的接口以获得良好的实践。
      • 您的 IArticle 接口不是有效的 TypeScript,我不清楚您要完成什么。考虑在像 this 这样的 TypeScript IDE 中测试您的代码,并确保您知道您的建议是有意义且有效的。
      • 是的,它不会,正确的应该是这个 - interface IArticle { main: document.querySelector("main > article"), aside: document.querySelector (“旁边>文章”)}
      • 不,这也不起作用。不能在接口定义中调用函数,也不能将Htmlarticle 属性设置为type。在发布代码之前,您真的应该尝试在 IDE 中测试您的代码。编译器应该告诉你这些东西,而不是 Stack Overflow 上的其他人。我在上面提供了一个链接,向您展示了发生了什么。
      猜你喜欢
      • 2012-12-31
      • 1970-01-01
      • 1970-01-01
      • 2020-02-24
      • 2017-02-01
      • 2013-09-19
      • 1970-01-01
      • 2020-10-30
      • 2019-10-28
      相关资源
      最近更新 更多