JavaScript 如何拆分 JSDoc 以及虛擬加載 TypeScript 定義檔

Lastor
Code 隨筆放置場
9 min readApr 24, 2021

日前在做一些 Facebook SparkAR 的專案,發現 Facebook 不知道用了甚麼魔法,居然可以在不裝任何套件的情況,讓 .js 檔可以進行 type check,也就是這種感覺。

本來一開始沒察覺有甚麼異樣,後來在寫其他專案的 .js 檔時,卻沒有這種 check 機制。研究半天發現了兩件事情:
1. 支援 TypeScript 的編輯器 (如 VSCode),可以用 TS 規範去 check JS
2. 這類編輯器也支援一種叫 JSDoc 的玩意,可以用註解的方式去定義 type

VSCode Setting 裡面有個 TypeScript 分類,裡面有個 Check JS 設定,勾選後編輯器就能在 .js 檔進行 TS check。

另外也可以在當前專案內建立 jsconfig.json 或是 tsconfig.json 進行局部設定。

{
"compilerOptions": {
"checkJs": true
}
}

如此,就能開啟如同上圖一般的 TS Check 功能。

除此之外,還有一個更簡單的方法,就是在想要 check 的 js 檔案頭部寫下這行。更快速容易的去切換是否要進行 TS Check。

// @ts-check

TS Check 本質上只是輔助我們寫 code,完全不會影響 js 本身的執行。我們更可以透過 JSDoc 在 js 檔案進行類似 TypeScript 的 interface 定義,目前許多 JS 套件都有透過 JSDoc 來撰寫定義檔與說明。

利用 JSDoc 定義一個虛擬 type,接著直接對變數進行斷言宣告,指定 type。接著 VScode 就能根據我們寫內容出現語法智能提示,並透過 TS Check 的監控,在寫 code 當下就能發現不符合規範的變數宣告或賦值。

當然,也可以跟很多套件一樣寫上中英文註解。

如此,依靠 TS Check 以及 JSDoc 兩者互相搭配,就能在不安裝任何套件的情況下(前提是編輯器支援),在純 JS 中做到類似 TS 的 type 控管,也省下很多寫文件的功夫,讓 script 本身成為一份文件。

拆分 type 定義檔

實際進行專案時,會碰到一個很實際的需求,就是 JSDoc 註解越寫越長,是否可以跟 TS 一樣,單獨拆出一個 index.d.ts 這樣的定義檔?另外,JSDoc 是寫在註解上,所以大包物件的聲明就會長得比較醜,不好閱讀。

於是稍微研究了一番,發現似乎可行。JSDoc 本身是可以伴隨著 import / export 讓其他文件也能讀取。

所以可以用一種類似 cheat 方式來實現。首先,新增一個 js 檔,並模仿 TS 的方式,加一個 .d 表示這是定義檔。

// module.d.js/**
* @typedef {object} Color
* @property {string} name
* @property {boolean} boo
*/
export default {}

假設定義了一個 Color 物件,屬性名示意一下隨便寫寫,然後 export 一個空物件。這邊要用 ES6 或是 CommonJS 的寫法都沒差,目的是為了騙 VSCode讓它認為這支檔案是一個 module。

然後在主檔案,用 JSDoc 把它 import 進來。

// main.js// @ts-check/**
* @typedef {import('./Color.d').Color} Color
*/
/** @type {Color} */
const Color = []

可以看到 VScode 成功導入了這支檔案的 type 聲明。由於要進行 import 的操作,所以目標檔案一定要是一個 module 才可以,這也是上面要寫一個空的 export 去騙編輯器的原因。

由於前面定義了 Color 是一個 object,下方宣告一個變數 Color 賦予一個空陣列,並斷言它是 type Color,這時 TS Check 就會出現紅線提示,提醒我們 Color 不應該是個 array。

這邊有一個概念很重要,像是 typedef 聲明出來的內容,是一種 Virtual Comments,是一個虛擬的註解,也就是最終 js 並不會執行它,它對於瀏覽器來說「只是個註解」。我們 import 的部分也是寫成一個註解的形式。

這就表示,這種寫法是可以直接丟給瀏覽器開啟的,瀏覽器並不會真的去加載這份 type 定義檔。這就給不啟用 npm 框架的傳統 Web 專案帶來了新的可能性,讓我們可以在不使用 Node.js,不安裝 TypeScript 的情況,來做到 type check,並且完全不影響瀏覽器正常運行原生 JS。

前面有提過,JSDoc 寫大包內容其實蠻醜的,按照上述的思路來走,既然這種作法不會影響瀏覽器 run 這支 JS,那是否表示…… 我也可以直接寫成 Typescript 定義檔?

答案是,可以的!

虛擬加載 Typescript 定義檔

一般來說,要使用 Typescript 來寫 code,就得安裝它的 npm 套件,寫完之後將其編譯為 JS,瀏覽器才能執行它。

然而定義檔與註解這部分,會發現除了 JSDoc 之外,也有人是直接寫成 TS 定義檔。我們可以在 npm 上下載該模組的 @types/xxx 定義檔,之後編輯器就能讀到這些型別聲明的的內容。

而且……「.js 其實也讀得到」

例如知名套件 jQuery,就有提供 TS 的定義檔可供 npm 下載。如果我們現在是一個非 npm 框架的 Web 傳統專案,使用 CDN 在 html 上加載 jQuery,那勢必編輯器就會對 $ 毫無反應,沒有語法提示。

這時候其實可以用 npm 去單獨下載 jQuery 的 TS 定義檔,然後我們會發現,我們的 js 檔莫名其妙可以讀取到 $ 的智能提示了。

既然套件做得到這件事情,就表示我們也能做到一樣的事情。

首先需要對 Typescript 有基本認知,它定義 type 時有兩種大類型,一種是全域宣告,另一種是 module 宣告 (如同上述拆分 JSDoc 的方式)。

jQuery 的 TS 定義檔,就是使用了全域宣告,讓我們的 js 檔可以在不進行 import 的情況,直接加載到 $ 的 type 聲明內容。

// @types/jquery, misc.d.ts// jquery 在其他檔案做完 JQueryStatic 的定義,最後做了一個全域宣告的動作
declare const jQuery: JQueryStatic;
declare const $: JQueryStatic;

只是這種宣告方式是宣告了一個「實」的東西出來,然而我們希望的是像 JSDoc 那樣,宣告一個「虛」的聲明。否則,我們在 js 檔去實作這個 interface 時,TS Check 會報錯,說它已被聲明過。

具體作法如下:

全域式聲明

先做出定義檔,隨便取個名子,既然是 global 的,就叫 gFoo.d.ts 吧。然後聲明一個 object 出來。由於是全域式的,不需要寫任何 export 關鍵字。

// gFoo.d.ts
interface globalFoo {
name: string
}

之後在主檔案,使用 TS 的三斜線來虛擬載入。

TS 三斜線指令:
https://willh.gitbook.io/typescript-tutorial/basics/declaration-files#xuan-gao-dang-an-zhong-de-yi-lai

// main.js// @ts-check/// <reference types="./gFoo" />/** @type {globalFoo} */
const globalFoo = {
name: '',
prop: 0,
}

如同前面 JSDoc 的作法,這是依靠註解來進行的載入,JSDoc 的格式同時兼容 Typescript,一樣可以透過 JSDoc 將 TS 定義的 interface 斷言給 JS 的變數。

同時,它終究還是個註解,跟 JSDoc 一樣,瀏覽器不會執行它。但是 VSCode 這邊依舊能成功抓到這份 TS 定義檔,並給我們智能提示以及 Type Check。

module 式聲明

這部分就跟 JSDoc 沒兩樣了,只是改用 Typescript 來寫。概念一樣是要加入 export 之類的關鍵字,來讓 VSCode 判定這是一個 module。

// Foo.d.tsinterface Foo {
name: string
}
export { Foo }

之後在主檔案引入它,但這邊不能使用三斜線,因為它是針對全域文件使用。所以這邊一樣使用 JSDoc 來加載。

// main.js// @ts-check/**
* @typedef {import('./Foo').Foo} Foo
*/
/** @type {Foo} */
const Foo = {
name: '',
prop: 0,
}

透過這樣的手法,就能在一定程度上享受 Typescript 的優勢來寫 JS,但同時又不被 Typescript 的龜毛限制給束縛。更重要的是,不用安裝 Typescript,也不用考慮編譯問題,非常的方便。

對 type 規範有興趣,但又不想全面 TS 化的人,可以考慮這種方案喔。

--

--

Lastor
Code 隨筆放置場

Web Frontend / 3D Modeling / Game and Animation. 設計本科生,前遊戲業 3D Artist,專擅日本動畫與遊戲相關領域。現在轉職為前端工程師,以專業遊戲美術的角度涉足 Web 前端開發。