跳至主要內容

類型和類型宣告

Deno 的設計原則之一是不使用非標準模組解析。TypeScript 在類型檢查檔案時,只會關注檔案的類型,而 tsc 編譯器有許多邏輯來嘗試解析這些類型。預設情況下,它會預期有附檔名的含糊模組指定符,並會嘗試在 .ts 指定符下尋找檔案,然後是 .d.ts,最後是 .js(當模組解析設定為 "node" 時,加上另一整組邏輯)。Deno 處理明確的指定符。

不過,這可能會造成幾個問題。例如,假設我想使用已轉譯為 JavaScript 的 TypeScript 檔案,以及類型定義檔。因此,我有 mod.jsmod.d.ts。如果我嘗試將 mod.js 匯入 Deno,它只會執行我要求它執行的動作,並匯入 mod.js,但這表示我的程式碼不會像 TypeScript 考慮 mod.d.ts 檔案取代 mod.js 檔案時那樣經過良好的類型檢查。

為了在 Deno 中支援這一點,Deno 有兩個解決方案,其中有一個解決方案的變體可以增強支援。您會遇到的兩個主要情況是

  • 作為 JavaScript 模組的匯入者,我知道應該套用哪些類型到模組。
  • 作為 JavaScript 模組的供應者,我知道應該套用哪些類型到模組。

後者是較好的情況,表示您作為模組的提供者或主機,所有人都可以在不用找出如何解析 JavaScript 模組的類型的情況下使用它,但當使用您可能無法直接控制的模組時,也需要執行前者的能力。

在匯入時提供類型

如果您正在使用 JavaScript 模組,而且您已建立類型(.d.ts 檔案)或已取得您想要使用的類型,您可以指示 Deno 在類型檢查時使用該檔案,而不是 JavaScript 檔案,方法是使用 @deno-types 編譯器提示。@deno-types 必須是單行雙斜線註解,在使用時會影響下一個匯入或重新匯出陳述式。

例如,如果我有 JavaScript 模組 coolLib.js,並且有一個單獨的 coolLib.d.ts 檔案想要使用,我會像這樣匯入

// @deno-types="./coolLib.d.ts"
import * as coolLib from "./coolLib.js";

在檔案中類型檢查 coolLib 及其使用情況時,將使用 coolLib.d.ts 類型,而不是查看 JavaScript 檔案。

編譯器提示的模式比對相當寬鬆,且會接受指定符號的引號和非引號值,以及等號前後的空白。

在主機時提供類型

如果您控制模組的原始碼,或控制檔案在網路伺服器上的主機方式,則有兩種方法可以告知 Deno 給定模組的類型,而不需要匯入者執行任何特殊操作。

使用三斜線參考指令

Deno 支援使用三斜線參考 types 指令,該指令採用 TypeScript 檔案中 TypeScript 使用的參考註解,以包含其他檔案,且僅套用於 JavaScript 檔案。

例如,如果我建立了 coolLib.js,並在它旁邊在 coolLib.d.ts 中為我的函式庫建立了類型定義,我可以在 coolLib.js 檔案中執行下列動作

/// <reference types="./coolLib.d.ts" />

// ... the rest of the JavaScript ...

當 Deno 遇到此指令時,它會解析 ./coolLib.d.ts 檔案,在 TypeScript 檢查檔案類型時使用它,而不是 JavaScript 檔案,但在執行程式時仍然載入 JavaScript 檔案。

ℹ️ 注意這是針對 TypeScript 的重新調整指令,僅適用於 JavaScript 檔案。在 TypeScript 檔案中使用 types 的三斜線參考指令在 Deno 中也適用,但其行為與 path 指令基本上相同。

使用 X-TypeScript-Types 標頭

與三斜線指令類似,Deno 支援遠端模組的標頭,指示 Deno 在何處找到特定模組的類型。例如,https://example.com/coolLib.js 的回應可能如下所示

HTTP/1.1 200 OK
Content-Type: application/javascript; charset=UTF-8
Content-Length: 648
X-TypeScript-Types: ./coolLib.d.ts

看到此標頭時,Deno 會嘗試擷取 https://example.com/coolLib.d.ts,並在檢查原始模組類型時使用它。

使用環境或全域類型

總體而言,最好在 Deno 中使用模組/UMD 類型定義,其中模組明確匯入其依賴的類型。模組化類型定義可以透過類型定義中的 declare global 表達擴充全域範圍。例如

declare global {
var AGlobalString: string;
}

這會在匯入類型定義時,讓 AGlobalString 在全域命名空間中可用。

不過,在某些情況下,當利用其他現有的類型函式庫時,可能無法利用模組化類型定義。因此,在檢查程式類型時,有方法可以包含任意類型定義。

使用三斜線指令

此選項將類型定義與程式碼本身結合。在模組類型附近加入三斜線 types 指令,檔案的類型檢查將包含類型定義。例如

/// <reference types="./types.d.ts" />

提供的指定符會像 Deno 中的任何其他指定符一樣解析,這表示它需要一個副檔名,並且與參考它的模組相關。它也可以是一個完全限定的 URL

/// <reference types="https://deno.land/x/pkg@1.0.0/types.d.ts" />

使用設定檔

另一個選項是使用設定為包含類型定義的設定檔,方法是提供 "types" 值給 "compilerOptions"。例如

{
"compilerOptions": {
"types": [
"./types.d.ts",
"https://deno.land/x/pkg@1.0.0/types.d.ts",
"/Users/me/pkg/types.d.ts"
]
}
}

與上述的三斜線參考一樣,"types" 陣列中提供的指定符將會像 Deno 中的其他指定符一樣解析。如果是相對指定符,它將會解析為相對於設定檔路徑。請務必透過指定 --config=path/to/file 旗標告訴 Deno 使用此檔案。

類型檢查 Web Workers

當 Deno 在 Web Worker 中載入 TypeScript 模組時,它會自動針對該模組及其相依性進行類型檢查,並與 Deno Web Worker 函式庫進行比較。這可能會在其他情況下造成挑戰,例如 deno cache 或在編輯器中。有幾種方法可以指示 Deno 使用 Worker 函式庫,而不是標準的 Deno 函式庫。

使用三斜線指令

此選項將函式庫設定與程式碼本身結合。透過在工作人員指令碼的進入點檔案頂端附近加入下列三斜線指令,Deno 現在會將其類型檢查為 Deno 工作人員指令碼,而與模組如何分析無關

/// <reference no-default-lib="true" />
/// <reference lib="deno.worker" />

第一個指令確保不會使用其他預設函式庫。如果省略此指令,您將會得到一些衝突的類型定義,因為 Deno 也會嘗試套用標準 Deno 函式庫。第二個指令指示 Deno 套用內建 Deno 工作人員類型定義加上相依函式庫(例如 "esnext")。

當您執行 deno cachedeno bundle 指令,或使用使用 Deno 語言伺服器的 IDE 時,Deno 應該會自動偵測這些指令,並在類型檢查時套用正確的函式庫。

此方法有一個缺點,就是它讓程式碼較難移植到其他非 Deno 平台,例如 tsc,因為只有 Deno 內建 "deno.worker" 函式庫。

使用組態檔

另一個選項是使用組態檔,該組態檔已設定套用函式庫檔案。一個能正常運作的最小檔案看起來會像這樣

{
"compilerOptions": {
"target": "esnext",
"lib": ["deno.worker"]
}
}

然後在命令列上執行指令時,您需要傳遞 --config path/to/file 參數,或者如果您使用的是利用 Deno 語言伺服器的 IDE,則設定 deno.config 設定。

如果您也有非工作人員指令碼,您必須省略 --config 參數,或設定一個符合您的非工作人員指令碼需求的參數。

重要事項

類型宣告語意

類型宣告檔案 (.d.ts 檔案) 遵循與 Deno 中其他檔案相同的語意。這表示宣告檔案假設為模組宣告 (UMD 宣告),而非環境/全域宣告。Deno 如何處理環境/全域宣告是無法預測的。

此外,如果類型宣告匯入其他項目,例如另一個 .d.ts 檔案,其解析遵循 Deno 的一般匯入規則。對於網路上產生且可用的許多 .d.ts 檔案,它們可能與 Deno 不相容。

為了克服這個問題,一些解決方案提供者,例如 Skypack CDN,會自動將類型宣告打包,就像它們提供 JavaScript 的 ESM 套件一樣。

Deno 友善 CDN

有些 CDN 會主機與 Deno 整合良好的 JavaScript 模組。

  • esm.sh 是預設提供類型宣告的 CDN (透過 X-TypeScript-Types 標頭)。它可以透過將 ?no-dts 附加到匯入 URL 來停用

    import React from "https://esm.sh/react?no-dts";
  • Skypack.dev 是另一個 CDN,它也會在您將 ?dts 附加為查詢字串至您的遠端模組匯入陳述式時提供類型宣告 (透過 X-TypeScript-Types 標頭)。以下是一個範例

    import React from "https://cdn.skypack.dev/react?dts";

類型檢查時 JavaScript 的行為

如果您在 Deno 中將 JavaScript 匯入 TypeScript,並且沒有類型,即使您已將 checkJs 設定為 false(Deno 的預設值),TypeScript 編譯器仍會存取 JavaScript 模組,並嘗試對其執行一些靜態分析,至少嘗試確定該模組的匯出外觀,以驗證 TypeScript 檔案中的匯入。

當嘗試匯入「常規」ES 模組時,這通常不會造成問題,但在某些情況下,如果模組有特殊封裝,或是一個全域 UMD 模組,TypeScript 對模組的分析可能會失敗,並造成誤導性錯誤。在這種情況下,最好的方法是使用上述方法之一提供一些形式的類型。

內部結構

雖然不需要了解 Deno 的內部運作方式才能善用 Deno 中的 TypeScript,但了解其運作方式會有幫助。

在執行或編譯任何程式碼之前,Deno 會透過剖析根模組來產生模組圖,然後偵測其所有相依性,接著遞迴地擷取並剖析這些模組,直到擷取所有相依性為止。

對於每個相依性,都有兩個潛在的「插槽」可用。一個是程式碼插槽,另一個是類型插槽。當填寫模組圖時,如果模組是可以或可以發佈到 JavaScript 的內容,它會填寫程式碼插槽,而只有類型相依性(例如 .d.ts 檔案)會填寫類型插槽。

當建立模組圖,並且需要對圖進行類型檢查時,Deno 會啟動 TypeScript 編譯器,並提供可能需要發佈為 JavaScript 的模組名稱。在該程序中,TypeScript 編譯器會要求其他模組,而 Deno 會查看相依性的插槽,在提供程式碼插槽之前,先提供類型插槽(如果已填寫)。

這表示當您導入 .d.ts 模組,或您使用上述其中一個解決方案為 JavaScript 程式碼提供替代型別模組時,就是將其提供給 TypeScript,而不是在解析模組時提供。