網站搜尋

javascript - 為什麼 getAttribute 在類型 "Node" 上不存在?


我正在將我寫的一些 Javascript 轉換成 Deno TypeScript 模組。這個腳本使用 deno-dom 來獲取頁面上的所有 "a[href]" 鏈接,並返回鏈接列表。腳本運行正常,但是,當我使用 deno test 測試代碼時,它會報錯

"TS2339 [ERROR]: Property 'getAttribute' does not exist on type 'Node'"

我的理解是,Element 類型是一種 Node 類型的「類型」——但我試圖智能地宣告這一點卻失敗了。我在這裡誤解了什麼?

這裡是有問題的完整函數:

import { DOMParser } from "https://deno.land/x/deno_dom/deno-dom-wasm.ts";

export async function getAllLinks(
  url: string,
  slice = 5,
): Promise<{ source: string; links: string[] }> {
  try {
    const response = await fetch(url);
    const content: string = await response.text();

    // Parse the HTML content using JSDOM
    const document = new DOMParser().parseFromString(content, "text/html");

    if (document === null) throw new Error("Failed to parse HTML");
    const baseUrl = response.url;

    // Find all links on the page
    const linkSelector = document.querySelectorAll(
      'a[href^="http:"], a[href^="https:"]',
    );

    if (linkSelector === undefined || linkSelector === null){
      throw new Error("No links found");
    }
    const links = Array.from(linkSelector, (link: Element) => link.getAttribute("href"))
      .map((link: string) => {
        // Convert relative links to absolute URLs
        const absoluteURL = new URL(link, baseUrl);
        return absoluteURL.href;
      })
      .filter((link: string) => {
        // Filter out links that are on the same domain as the URL
        const parsedUrl = new URL(url);
        const parsedLink = new URL(link);
        return parsedUrl.hostname !== parsedLink.hostname;
      });

    return {
      source: baseUrl,
      links: links.slice(0, slice),
    };
  } catch (error) {
    console.error(error);
    throw error;
  }
}

謝謝。

編輯:將 (link) 宣告為 (link: Element) 會導致 Array.from 中它之前的 linkSelector 出現此錯誤:

No overload matches this call.
  Overload 1 of 4, '(iterable: Iterable<Element> | ArrayLike<Element>, mapfn: (v: Element, k: number) => string | null, thisArg?: any): (string | null)[]', gave the following error.
    Argument of type 'NodeList' is not assignable to parameter of type 'Iterable<Element> | ArrayLike<Element>'.
      Type 'NodeList' is not assignable to type 'ArrayLike<Element>'.
        'number' index signatures are incompatible.
          Type 'Node' is missing the following properties from type 'Element': attributes, classList, className, clientHeight, and 127 more.  Overload 2 of 4, '(arrayLike: ArrayLike<Element>, mapfn: (v: Element, k: number) => string | null, thisArg?: any): (string | null)[]', gave the following error.
    Argument of type 'NodeList' is not assignable to parameter of type 'ArrayLike<Element>'.deno-ts(2769)

並且還會因這個問題中斷下一行 (link: string) 的宣告:

argument of type '(link: string) => string' is not assignable to parameter of type '(value: string | null, index: number, array: (string | null)[]) => string'.
  Types of parameters 'link' and 'value' are incompatible.
    Type 'string | null' is not assignable to type 'string'.
      Type 'null' is not assignable to type 'string'.deno-ts(2345)

更令人困惑的是,不管我選擇哪種類型聲明,程式碼仍然可以完全正常執行。

答案#1

這是 deno_dom 函式庫中的一個未解決問題,並且它仍然存在(至少在我撰寫此答案時 — 版本為 0.1.38)。更多信息請參見以下 GitHub 問題:

b-fuze/deno-dom#4 — 錯誤:querySelectorAll 返回 NodeListOf<Node> 而不是 NodeListOf<Element>

那個討論串包含了所有資訊和相關資源的連結,但這裡是摘要:

deno_dom 並沒有完全或正確地實現 DOM 規範,而且它的型別也與 TypeScript 的 DOM 型別不一致。

在 TS DOM 庫中,document.querySelectorAll 的返回類型是 NodeListOf<E>,其中 E 是一個受限於 Element 的泛型參數,而 Element 是實現了 getAttribute 方法的接口。

在 deno_dom 中,document.querySelectorAll 的返回類型是 NodeList,它是 Node 的可迭代物件(Node 不包含 getAttribute 方法 —— 所有 Element 都是 Node,但反之則不然)。

NodeList 的實際元素確實是 Element,但只是沒有這樣被類型化,所以——在庫被修復之前——你將需要使用類型斷言來告訴編譯器實際情況。這裡是一個自包含的示例:

example.ts:

import { assert } from "https://deno.land/[email /testing/asserts.ts";

import {
  DOMParser,
  type Element,
} from "https://deno.land/x/[email /deno-dom-wasm.ts";

const html = `
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>Ice cream flavors</title>
</head>
<body>
  <h1>Ice cream</h1>
  <h2>Basic flavors</h2>
  <ul class="flavors basic">
    <li data-flavor="vanilla">Vanilla</li>
    <li data-flavor="chocolate">Chocolate</li>
    <li data-flavor="strawberry">Strawberry</li>
  </ul>
</body>
</html>
`;

const document = new DOMParser().parseFromString(html, "text/html");
//    ^? const document: HTMLDocument | null
assert(document, "The document could not be parsed");

const flavorElements = document.querySelectorAll(
  "ul.flavors.basic > li",
) as Iterable<Element>; /*
  ^^^^^^^^^^^^^^^^^^^^
Use a type assertion here */

const flavors = [...flavorElements].map((element) => {
  const flavor = element.getAttribute("data-flavor");
  //    ^? const flavor: string | null
  assert(flavor, "Flavor attribute not found");
  return flavor;
});

console.log(flavors); // ["vanilla", "chocolate", "strawberry"]

在終端機中:

% deno --version
deno 1.35.0 (release, aarch64-apple-darwin)
v8 11.6.189.7
typescript 5.1.6

% deno run --check example.ts
Check file:///Users/deno/example.ts
[ "vanilla", "chocolate", "strawberry" ]

作為比較,這是 TypeScript Playground 中的等效程式碼。