如果使用未知的標籤名稱調用 document.createElement,TypeScript 能夠報錯嗎?
TypeScript 似乎對以下任一行單獨使用都沒問題:
const a = document.createElement("a");
const A = document.createElement("A");
然而,僅有下列其中一項有效:
a.href = ""; // Works
A.href = ""; // error TS2339: Property 'href' does not exist on type 'HTMLElement'.
所以看起來 TypeScript 只支援小寫標籤名稱(坦白說我同意這點),但它也沒有提供很多方法來提前捕捉錯誤,而是讓事情一直過去,直到我嘗試使用較不抽象的 DOM 介面時才會出現問題。如果我傳入拼錯的標籤名稱,也會有相同的效果。這有點合理,因為 document.createElement() 會接受非標準標籤字串並忠實地創建該名稱的標籤,但對於一些我事先知道自己永遠不會想要或需要做的情況而言,這種行為就顯得不必要了。
有沒有任何「迂腐」的選項/旗標/etc. 可以設定,讓它在我試圖創建一個它尚未知道的元素時大聲提示我?我最好限制自己使用 W3C 定義的標籤,但我也歡迎任何會在我打錯字的 document.createElement() 上顯示錯誤訊息的方法。
我已經搜尋過這個問題,但我有點懷疑這可能只有在你知道要搜尋哪些術語時才容易回答,而我到目前為止還沒找到有效的方法。我找到的大部分資訊都是關於如何啟用自訂標籤或如何防止錯誤訊息,而這正好與我想做的相反。
答案#1
沒有任何編譯器選項可以針對 createElement(),它只是 TypeScript 庫 lib.dom.d.ts 中的一組呼叫簽名:
interface Document {
createElement<K extends keyof HTMLElementTagNameMap>(
tagName: K, options?: ElementCreationOptions
): HTMLElementTagNameMap[K];
/** @deprecated */
createElement<K extends keyof HTMLElementDeprecatedTagNameMap>(
tagName: K, options?: ElementCreationOptions
): HTMLElementDeprecatedTagNameMap[K];
createElement(
tagName: string, options?: ElementCreationOptions
): HTMLElement;
}
大概你對第三個呼叫簽名的存在有問題,其中 tagName 可以是任何 string,並且它會返回一個 HTMLElement。理想情況下,你希望能夠直接 移除 那個呼叫簽名(也可能包括前一個)。但你無法像那樣動態地移除呼叫簽名。你可以透過聲明合併動態 新增 呼叫簽名,但要移除它們則需要先實現像 microsoft/TypeScript#19064 這樣的功能,而這尚未實現。
所以除了放棄之外,你的選擇是:
自己分叉 lib.dom.d.ts 函式庫 並使用它來取代 TypeScript 提供的版本。這涉及到複製該文件、移除有問題的呼叫簽章,然後將你的 --lib 編譯器選項改成使用你自己的版本。如果你沒有 --lib 選項,因為你使用的是為你所宣告的 --target 版本提供的預設函式庫,那麼你需要新增一個對應的 --lib 選項。這種方法可行,但非常費時,並且需要你維護本地版本的函式庫。
或
將新的呼叫簽名合併到現有的函式庫中,這些簽名將優先於提供的簽名,並使它們生成的內容甚至比 HTMLElement 更不可用。由於你無法移除呼叫簽名,你唯一能做的就是像這樣“重新添加”它們
// merging in
interface Document {
// existing one which we like
createElement<K extends keyof HTMLElementTagNameMap>(
tagName: K, options?: ElementCreationOptions
): HTMLElementTagNameMap[K];
// one we dislike
/** @deprecated */
createElement(tagName: string, options?: ElementCreationOptions): never;
}
現在如果你用隨機的 string 調用 createElement(),它會生成 never 類型的結果,這會對更多的事情產生警告。
另外,它被標記為 //@deprecated,所以根據你的程式碼如何編譯,這個呼叫本身可能會導致診斷訊息:
const a = document.createElement("a"); // okay
a.innerHTML; // okay
const A = document.createElement("A"); // warning!
// ^^^^^^^^^^^^^
/* The signature '(tagName: string, options?: ElementCreationOptions | undefined): never' of
'document.createElement' is deprecated.(6387) */
A.innerHTML; // error! Property 'innerHTML' does not exist on type 'never'.
不幸的是,雖然 //@deprecated 警告會在大多數 IDE 中顯示,但在 tsc(TypeScript 編譯器本身)中幾乎沒有任何作用。在 microsoft/TypeScript#49433 中曾有此請求,但目前沒有計畫。你可以在構建中使用 typescript-eslint 的情況下使用 @typescript-eslint/no-deprecated 規則,但同樣,這不是 tsc 本身的一部分,而且很可能永遠不會成為。
這並不是你嚴格要求的,但在不分叉該庫的情況下,這已經是你能做到的最接近了。
程式碼的 Playground 連結
答案#2
DOM 類型定義在 lib.dom.d.ts。
createElement 和其他相關的 API 使用 HTMLElementTagNameMap 介面來決定回傳類型。如你所見,它只包含小寫的鍵。
所以這個映射是內建在類型中的,你無法改變索引訪問的行為來支持大寫鍵。
你可以做的是自己擴展它以支持大寫鍵。
interface HTMLElementTagNameMap {
"A": HTMLAnchorElement;
}
const A = document.createElement('A')
A.href = '...'
